@h-rig/rig-extension 0.0.6-alpha.100
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/README.md +1 -0
- package/dist/src/board-theme.js +150 -0
- package/dist/src/board-views.js +475 -0
- package/dist/src/extension.js +3757 -0
- package/dist/src/workflow/identity.js +429 -0
- package/dist/src/workflow/projections.js +389 -0
- package/package.json +36 -0
|
@@ -0,0 +1,3757 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/rig-extension/src/extension.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { dirname, resolve } from "path";
|
|
8
|
+
import {
|
|
9
|
+
SessionManager
|
|
10
|
+
} from "@oh-my-pi/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
listCollabSessionProjections
|
|
13
|
+
} from "@oh-my-pi/pi-coding-agent/collab/api";
|
|
14
|
+
import { buildStopSentinel, CUSTOM_TYPE_FOR, foldRunSessionEntries, parseStopSentinel, RIG_RUN_STEERING, sessionIdFromSessionFile } from "@rig/contracts";
|
|
15
|
+
import { buildRigInitConfigSource, computeTaskDependencyBadges, selectNextReadyTaskByPriority, selectTasksAssignedToMe, selectTasksGroupedByStatus } from "@rig/core";
|
|
16
|
+
import { createEnvCloseoutRunners } from "@rig/runtime/control-plane/native/closeout-runners";
|
|
17
|
+
import { runInProcessCloseout } from "@rig/runtime/control-plane/native/in-process-closeout";
|
|
18
|
+
import { projectRunFromSession } from "@rig/runtime/control-plane/run-session-projection";
|
|
19
|
+
import { listManagedRemoteEndpoints, resolveOwnerNamespaceKey, resolveRegistryBaseUrl, resolveRegistrySecret, resolveRelayUrl, resolveSelectedRemote, upsertManagedRemoteEndpoint } from "@rig/runtime/control-plane/remote";
|
|
20
|
+
import { resolveAuthorityPaths } from "@rig/runtime/control-plane/server-paths";
|
|
21
|
+
import { listAgentRuntimes } from "@rig/runtime/control-plane/runtime/isolation";
|
|
22
|
+
import { updateRunTaskSourceLifecycle } from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
23
|
+
import { dispatchEventToTargets, loadNotificationConfig } from "@rig/runtime/control-plane/notifications";
|
|
24
|
+
import {
|
|
25
|
+
beginGitHubDeviceFlow,
|
|
26
|
+
createGitHubAuthStore,
|
|
27
|
+
pollGitHubDeviceFlow,
|
|
28
|
+
probeGitHubRepository,
|
|
29
|
+
resolveGitHubAuthStatus,
|
|
30
|
+
saveGitHubTokenForProject
|
|
31
|
+
} from "@rig/runtime/control-plane/github";
|
|
32
|
+
import { createRegistryClient } from "@rig/relay-registry";
|
|
33
|
+
import { deriveOwnerToken } from "@rig/relay-registry/auth";
|
|
34
|
+
import { isKeyRelease, matchesKey } from "@oh-my-pi/pi-tui";
|
|
35
|
+
import { Duration, Effect, Fiber, Stream } from "effect";
|
|
36
|
+
import { localRunChanges, remoteRunChanges } from "@rig/runtime/control-plane/run-discovery-stream";
|
|
37
|
+
import { forkDiscoverySources, RunDiscoveryBus } from "@rig/runtime/control-plane/run-discovery-bus";
|
|
38
|
+
|
|
39
|
+
// packages/rig-extension/src/board-views.ts
|
|
40
|
+
import { helpCatalog } from "@rig/contracts";
|
|
41
|
+
import { truncateToWidth } from "@oh-my-pi/pi-tui";
|
|
42
|
+
|
|
43
|
+
// packages/rig-extension/src/board-theme.ts
|
|
44
|
+
var RIG_PALETTE = {
|
|
45
|
+
ink: "#f2f3f6",
|
|
46
|
+
ink2: "#aeb0ba",
|
|
47
|
+
ink3: "#6c6e79",
|
|
48
|
+
ink4: "#44464f",
|
|
49
|
+
accent: "#ccff4d",
|
|
50
|
+
accentDim: "#a9d63f",
|
|
51
|
+
cyan: "#56d8ff",
|
|
52
|
+
red: "#ff5d5d",
|
|
53
|
+
yellow: "#ffd24d"
|
|
54
|
+
};
|
|
55
|
+
function hexToRgb(hex) {
|
|
56
|
+
const value = hex.replace("#", "");
|
|
57
|
+
return [
|
|
58
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
59
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
60
|
+
Number.parseInt(value.slice(4, 6), 16)
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
function fg(hex) {
|
|
64
|
+
const [r, g, b] = hexToRgb(hex);
|
|
65
|
+
return (text) => `\x1B[38;2;${r};${g};${b}m${text}\x1B[39m`;
|
|
66
|
+
}
|
|
67
|
+
var ink = fg(RIG_PALETTE.ink);
|
|
68
|
+
var ink2 = fg(RIG_PALETTE.ink2);
|
|
69
|
+
var ink3 = fg(RIG_PALETTE.ink3);
|
|
70
|
+
var ink4 = fg(RIG_PALETTE.ink4);
|
|
71
|
+
var accent = fg(RIG_PALETTE.accent);
|
|
72
|
+
var accentDim = fg(RIG_PALETTE.accentDim);
|
|
73
|
+
var cyan = fg(RIG_PALETTE.cyan);
|
|
74
|
+
var red = fg(RIG_PALETTE.red);
|
|
75
|
+
var yellow = fg(RIG_PALETTE.yellow);
|
|
76
|
+
function bold(text) {
|
|
77
|
+
return `\x1B[1m${text}\x1B[22m`;
|
|
78
|
+
}
|
|
79
|
+
function statusColor(status) {
|
|
80
|
+
switch (status) {
|
|
81
|
+
case "running":
|
|
82
|
+
return accent;
|
|
83
|
+
case "preparing":
|
|
84
|
+
case "created":
|
|
85
|
+
case "validating":
|
|
86
|
+
case "reviewing":
|
|
87
|
+
case "closing-out":
|
|
88
|
+
return cyan;
|
|
89
|
+
case "needs-attention":
|
|
90
|
+
case "needs_attention":
|
|
91
|
+
return yellow;
|
|
92
|
+
case "failed":
|
|
93
|
+
return red;
|
|
94
|
+
default:
|
|
95
|
+
return ink3;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var RIG_SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
99
|
+
var DRONE_ART = [
|
|
100
|
+
" .-=-. .-=-. ",
|
|
101
|
+
" ( !!! ) ( !!! ) ",
|
|
102
|
+
" '-=-'._ _.'-=-' ",
|
|
103
|
+
" '._ _.' ",
|
|
104
|
+
" '=$$$$$$$=.' ",
|
|
105
|
+
" =$$$$$$$$$$$= ",
|
|
106
|
+
" $$$@@@@@@@@@@$$$ ",
|
|
107
|
+
" $$$@@ @@$$$ ",
|
|
108
|
+
" $$@ ? @$$$ ",
|
|
109
|
+
" $$$@ '-' @$$$ ",
|
|
110
|
+
" $$$@@ @@$$$ ",
|
|
111
|
+
" $$$@@@@@@@@@@$$$ ",
|
|
112
|
+
" =$$$$$$$$$$$= ",
|
|
113
|
+
" '=$$$$$$$=.' ",
|
|
114
|
+
" _.' '._ ",
|
|
115
|
+
" .-=-.' '.-=-. ",
|
|
116
|
+
" ( !!! ) ( !!! ) ",
|
|
117
|
+
" '-=-' '-=-' "
|
|
118
|
+
];
|
|
119
|
+
var BLADE_FRAMES = ["---", "\\\\\\", "|||", "///"];
|
|
120
|
+
var EYE_FRAMES = ["@", "o", "."];
|
|
121
|
+
function droneCharColor(char) {
|
|
122
|
+
if (char === "$" || char === "@")
|
|
123
|
+
return accent;
|
|
124
|
+
if (char === "=" || char === "%")
|
|
125
|
+
return accentDim;
|
|
126
|
+
if (char === "\\" || char === "/")
|
|
127
|
+
return ink3;
|
|
128
|
+
return ink4;
|
|
129
|
+
}
|
|
130
|
+
function renderDroneFrame(tick) {
|
|
131
|
+
const blade = BLADE_FRAMES[Math.floor(tick / 4) % BLADE_FRAMES.length];
|
|
132
|
+
const pulse = Math.sin(tick * 0.07);
|
|
133
|
+
const eye = pulse > 0.45 ? EYE_FRAMES[0] : pulse > -0.1 ? EYE_FRAMES[1] : EYE_FRAMES[2];
|
|
134
|
+
return DRONE_ART.map((line) => {
|
|
135
|
+
let out = "";
|
|
136
|
+
let index = 0;
|
|
137
|
+
while (index < line.length) {
|
|
138
|
+
if (line.startsWith("!!!", index)) {
|
|
139
|
+
out += cyan(blade);
|
|
140
|
+
index += 3;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const char = line[index];
|
|
144
|
+
if (char === "?") {
|
|
145
|
+
out += bold(cyan(eye));
|
|
146
|
+
} else if (char !== " ") {
|
|
147
|
+
out += droneCharColor(char)(char);
|
|
148
|
+
} else {
|
|
149
|
+
out += " ";
|
|
150
|
+
}
|
|
151
|
+
index += 1;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
var DRONE_WIDTH = DRONE_ART[0].length;
|
|
157
|
+
var DRONE_HEIGHT = DRONE_ART.length;
|
|
158
|
+
var MICRO_BLADES = ["---", "\\\\\\", "|||", "///"];
|
|
159
|
+
function microDroneFrame(tick) {
|
|
160
|
+
const blade = MICRO_BLADES[tick % MICRO_BLADES.length];
|
|
161
|
+
const eye = EYE_FRAMES[Math.floor(tick / 2) % EYE_FRAMES.length];
|
|
162
|
+
return `(${blade})${eye}(${blade})`;
|
|
163
|
+
}
|
|
164
|
+
var MICRO_DRONE_FRAMES = Array.from({ length: 12 }, (_, index) => microDroneFrame(index));
|
|
165
|
+
|
|
166
|
+
// packages/rig-extension/src/board-views.ts
|
|
167
|
+
function hairline(width) {
|
|
168
|
+
return ink4("\u2500".repeat(Math.max(0, width)));
|
|
169
|
+
}
|
|
170
|
+
function statusRank(status) {
|
|
171
|
+
switch (status) {
|
|
172
|
+
case "needs-attention":
|
|
173
|
+
case "needs_attention":
|
|
174
|
+
return 0;
|
|
175
|
+
case "running":
|
|
176
|
+
return 1;
|
|
177
|
+
case "pending":
|
|
178
|
+
case "preparing":
|
|
179
|
+
case "adopted":
|
|
180
|
+
return 2;
|
|
181
|
+
case "completed":
|
|
182
|
+
case "merged":
|
|
183
|
+
return 3;
|
|
184
|
+
case "failed":
|
|
185
|
+
return 4;
|
|
186
|
+
default:
|
|
187
|
+
return 5;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class RunsList {
|
|
192
|
+
runs = [];
|
|
193
|
+
selected = 0;
|
|
194
|
+
loading = true;
|
|
195
|
+
noProject = false;
|
|
196
|
+
tick = 0;
|
|
197
|
+
filter = "";
|
|
198
|
+
sortMode = "active";
|
|
199
|
+
visibleRuns() {
|
|
200
|
+
const query = this.filter.trim().toLowerCase();
|
|
201
|
+
const filtered = query ? this.runs.filter((run) => run.runId.toLowerCase().includes(query) || run.status.toLowerCase().includes(query) || run.title.toLowerCase().includes(query)) : [...this.runs];
|
|
202
|
+
if (this.sortMode === "active") {
|
|
203
|
+
return filtered.map((run, index) => ({ run, index })).sort((a, b) => statusRank(a.run.status) - statusRank(b.run.status) || a.index - b.index).map((entry) => entry.run);
|
|
204
|
+
}
|
|
205
|
+
if (this.sortMode === "status") {
|
|
206
|
+
return filtered.map((run, index) => ({ run, index })).sort((a, b) => a.run.status.localeCompare(b.run.status) || a.index - b.index).map((entry) => entry.run);
|
|
207
|
+
}
|
|
208
|
+
return filtered;
|
|
209
|
+
}
|
|
210
|
+
moveSelection(delta) {
|
|
211
|
+
const visible = this.visibleRuns();
|
|
212
|
+
if (visible.length === 0)
|
|
213
|
+
return;
|
|
214
|
+
this.selected = Math.max(0, Math.min(visible.length - 1, this.selected + delta));
|
|
215
|
+
}
|
|
216
|
+
selectedRun() {
|
|
217
|
+
return this.visibleRuns()[this.selected] ?? null;
|
|
218
|
+
}
|
|
219
|
+
cycleSort() {
|
|
220
|
+
this.sortMode = this.sortMode === "active" ? "recent" : this.sortMode === "recent" ? "status" : "active";
|
|
221
|
+
this.clampSelection();
|
|
222
|
+
return this.sortMode;
|
|
223
|
+
}
|
|
224
|
+
setFilter(filter) {
|
|
225
|
+
this.filter = filter;
|
|
226
|
+
this.clampSelection();
|
|
227
|
+
}
|
|
228
|
+
clampSelection() {
|
|
229
|
+
this.selected = Math.max(0, Math.min(this.selected, Math.max(0, this.visibleRuns().length - 1)));
|
|
230
|
+
}
|
|
231
|
+
setRuns(runs) {
|
|
232
|
+
const previous = this.selectedRun()?.runId;
|
|
233
|
+
this.runs = runs;
|
|
234
|
+
if (previous) {
|
|
235
|
+
const index = this.visibleRuns().findIndex((run) => run.runId === previous);
|
|
236
|
+
if (index >= 0)
|
|
237
|
+
this.selected = index;
|
|
238
|
+
}
|
|
239
|
+
this.clampSelection();
|
|
240
|
+
this.loading = false;
|
|
241
|
+
}
|
|
242
|
+
invalidate() {}
|
|
243
|
+
render(width) {
|
|
244
|
+
return this.renderLines(width).map((line) => truncateToWidth(line, Math.max(10, width - 1)));
|
|
245
|
+
}
|
|
246
|
+
renderLines(width) {
|
|
247
|
+
if (this.loading) {
|
|
248
|
+
const frame = renderDroneFrame(this.tick);
|
|
249
|
+
const spinner = RIG_SPINNER_FRAMES[this.tick % RIG_SPINNER_FRAMES.length];
|
|
250
|
+
const pad = Math.max(0, Math.floor((width - 34) / 2));
|
|
251
|
+
return [
|
|
252
|
+
"",
|
|
253
|
+
...frame.map((line) => " ".repeat(pad) + line),
|
|
254
|
+
"",
|
|
255
|
+
" ".repeat(Math.max(0, Math.floor((width - 24) / 2))) + accent(spinner) + ink3(" contacting the fleet\u2026"),
|
|
256
|
+
""
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
if (this.noProject) {
|
|
260
|
+
const frame = renderDroneFrame(this.tick);
|
|
261
|
+
const pad = Math.max(0, Math.floor((width - 34) / 2));
|
|
262
|
+
const center = (text, visible2) => " ".repeat(Math.max(0, Math.floor((width - visible2) / 2))) + text;
|
|
263
|
+
return [
|
|
264
|
+
"",
|
|
265
|
+
...frame.map((line) => " ".repeat(pad) + line),
|
|
266
|
+
"",
|
|
267
|
+
center(ink2("no rig project in this directory"), 32),
|
|
268
|
+
"",
|
|
269
|
+
center(`${accent("rig init")}${ink2(" set this repo up: config, GitHub auth, task source, server, Pi")}`, 80),
|
|
270
|
+
center(`${accent("rig init --yes")}${ink2(" same, non-interactive, sensible defaults")}`, 60),
|
|
271
|
+
center(`${accent("rig doctor")}${ink2(" already initialized somewhere else? check the wiring")}`, 70),
|
|
272
|
+
"",
|
|
273
|
+
center(ink3("after init: rig task run --next puts a drone on your next task"), 62),
|
|
274
|
+
""
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
const visible = this.visibleRuns();
|
|
278
|
+
if (this.runs.length === 0) {
|
|
279
|
+
const frame = renderDroneFrame(this.tick);
|
|
280
|
+
const pad = Math.max(0, Math.floor((width - 34) / 2));
|
|
281
|
+
return [
|
|
282
|
+
"",
|
|
283
|
+
...frame.map((line) => " ".repeat(pad) + line),
|
|
284
|
+
"",
|
|
285
|
+
" ".repeat(Math.max(0, Math.floor((width - 44) / 2))) + ink2("no runs yet \u2014 ") + accent("n") + ink2(" picks a task and launches one"),
|
|
286
|
+
""
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
if (visible.length === 0) {
|
|
290
|
+
return ["", ` ${ink2("no runs match")} ${accent(`/${this.filter}`)} ${ink3("\u2014 esc clears the filter")}`, ""];
|
|
291
|
+
}
|
|
292
|
+
const maxVisible = 16;
|
|
293
|
+
const start = Math.max(0, Math.min(this.selected - Math.floor(maxVisible / 2), visible.length - maxVisible));
|
|
294
|
+
const window = visible.slice(start, start + maxVisible);
|
|
295
|
+
const lines = window.map((run, index) => {
|
|
296
|
+
const absolute = start + index;
|
|
297
|
+
const isSelected = absolute === this.selected;
|
|
298
|
+
const dot = statusColor(run.status)(isSelected ? "\u25CF" : "\xB7");
|
|
299
|
+
const id = run.runId.slice(0, 8);
|
|
300
|
+
const status = statusColor(run.status)(run.status.padEnd(16));
|
|
301
|
+
const title = truncateToWidth(run.title, Math.max(8, width - 36));
|
|
302
|
+
const row = ` ${dot} ${isSelected ? bold(ink(id)) : ink3(id)} ${status} ${isSelected ? ink(title) : ink2(title)}`;
|
|
303
|
+
return isSelected ? accent("\u258C") + row : " " + row;
|
|
304
|
+
});
|
|
305
|
+
const meta = [];
|
|
306
|
+
if (visible.length > maxVisible)
|
|
307
|
+
meta.push(`${this.selected + 1}/${visible.length}`);
|
|
308
|
+
if (this.filter)
|
|
309
|
+
meta.push(`filter: ${this.filter}`);
|
|
310
|
+
meta.push(`sort: ${this.sortMode}`);
|
|
311
|
+
lines.push(ink4(` ${meta.join(" \xB7 ")}`));
|
|
312
|
+
return ["", ...lines, ""];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
class TasksView {
|
|
317
|
+
tasks = [];
|
|
318
|
+
selected = 0;
|
|
319
|
+
loading = true;
|
|
320
|
+
error = null;
|
|
321
|
+
tick = 0;
|
|
322
|
+
setTasks(tasks) {
|
|
323
|
+
this.tasks = tasks;
|
|
324
|
+
this.selected = Math.min(this.selected, Math.max(0, tasks.length - 1));
|
|
325
|
+
this.loading = false;
|
|
326
|
+
this.error = null;
|
|
327
|
+
}
|
|
328
|
+
moveSelection(delta) {
|
|
329
|
+
if (this.tasks.length === 0)
|
|
330
|
+
return;
|
|
331
|
+
this.selected = Math.max(0, Math.min(this.tasks.length - 1, this.selected + delta));
|
|
332
|
+
}
|
|
333
|
+
selectedTask() {
|
|
334
|
+
return this.tasks[this.selected] ?? null;
|
|
335
|
+
}
|
|
336
|
+
invalidate() {}
|
|
337
|
+
render(width) {
|
|
338
|
+
return this.renderLines(width).map((line) => truncateToWidth(line, Math.max(10, width - 1)));
|
|
339
|
+
}
|
|
340
|
+
renderLines(width) {
|
|
341
|
+
if (this.loading) {
|
|
342
|
+
const spinner = RIG_SPINNER_FRAMES[this.tick % RIG_SPINNER_FRAMES.length];
|
|
343
|
+
return ["", ` ${accent(spinner)} ${ink3("reading tasks from the server\u2026")}`, ""];
|
|
344
|
+
}
|
|
345
|
+
if (this.error) {
|
|
346
|
+
return ["", ` ${red("tasks unavailable:")} ${ink2(this.error)}`, ""];
|
|
347
|
+
}
|
|
348
|
+
if (this.tasks.length === 0) {
|
|
349
|
+
return ["", ` ${ink2("no ready tasks \u2014 the task source is drained")}`, ""];
|
|
350
|
+
}
|
|
351
|
+
const maxVisible = 16;
|
|
352
|
+
const start = Math.max(0, Math.min(this.selected - Math.floor(maxVisible / 2), this.tasks.length - maxVisible));
|
|
353
|
+
const window = this.tasks.slice(start, start + maxVisible);
|
|
354
|
+
const lines = window.map((task, index) => {
|
|
355
|
+
const absolute = start + index;
|
|
356
|
+
const isSelected = absolute === this.selected;
|
|
357
|
+
const id = task.id.padEnd(8).slice(0, 12);
|
|
358
|
+
const status = statusColor(task.status)(task.status.padEnd(12));
|
|
359
|
+
const title = truncateToWidth(task.title, Math.max(8, width - 32));
|
|
360
|
+
const row = ` ${isSelected ? bold(ink(id)) : ink3(id)} ${status} ${isSelected ? ink(title) : ink2(title)}`;
|
|
361
|
+
return isSelected ? accent("\u258C") + row : " " + row;
|
|
362
|
+
});
|
|
363
|
+
if (this.tasks.length > maxVisible)
|
|
364
|
+
lines.push(ink4(` ${this.selected + 1}/${this.tasks.length}`));
|
|
365
|
+
return ["", ...lines, ""];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
class InboxView {
|
|
370
|
+
items = [];
|
|
371
|
+
selected = 0;
|
|
372
|
+
loading = true;
|
|
373
|
+
error = null;
|
|
374
|
+
tick = 0;
|
|
375
|
+
setItems(items) {
|
|
376
|
+
this.items = items;
|
|
377
|
+
this.selected = Math.min(this.selected, Math.max(0, items.length - 1));
|
|
378
|
+
this.loading = false;
|
|
379
|
+
this.error = null;
|
|
380
|
+
}
|
|
381
|
+
moveSelection(delta) {
|
|
382
|
+
if (this.items.length === 0)
|
|
383
|
+
return;
|
|
384
|
+
this.selected = Math.max(0, Math.min(this.items.length - 1, this.selected + delta));
|
|
385
|
+
}
|
|
386
|
+
selectedItem() {
|
|
387
|
+
return this.items[this.selected] ?? null;
|
|
388
|
+
}
|
|
389
|
+
invalidate() {}
|
|
390
|
+
render(width) {
|
|
391
|
+
return this.renderLines(width).map((line) => truncateToWidth(line, Math.max(10, width - 1)));
|
|
392
|
+
}
|
|
393
|
+
renderLines(width) {
|
|
394
|
+
if (this.loading) {
|
|
395
|
+
const spinner = RIG_SPINNER_FRAMES[this.tick % RIG_SPINNER_FRAMES.length];
|
|
396
|
+
return ["", ` ${accent(spinner)} ${ink3("reading the inbox\u2026")}`, ""];
|
|
397
|
+
}
|
|
398
|
+
if (this.error) {
|
|
399
|
+
return ["", ` ${red("inbox unavailable:")} ${ink2(this.error)}`, ""];
|
|
400
|
+
}
|
|
401
|
+
if (this.items.length === 0) {
|
|
402
|
+
return ["", ` ${ink2("inbox clear \u2014 no drone is waiting on you")}`, ""];
|
|
403
|
+
}
|
|
404
|
+
const maxVisible = 14;
|
|
405
|
+
const start = Math.max(0, Math.min(this.selected - Math.floor(maxVisible / 2), this.items.length - maxVisible));
|
|
406
|
+
const window = this.items.slice(start, start + maxVisible);
|
|
407
|
+
const lines = window.flatMap((item, index) => {
|
|
408
|
+
const absolute = start + index;
|
|
409
|
+
const isSelected = absolute === this.selected;
|
|
410
|
+
const kind = item.kind === "approval" ? accentDim("approval") : cyan("input ");
|
|
411
|
+
const run = item.runId.slice(0, 8);
|
|
412
|
+
const summary = truncateToWidth(item.summary, Math.max(8, width - 30));
|
|
413
|
+
const row = ` ${kind} ${isSelected ? bold(ink(run)) : ink3(run)} ${isSelected ? ink(summary) : ink2(summary)}`;
|
|
414
|
+
const main = isSelected ? accent("\u258C") + row : " " + row;
|
|
415
|
+
if (isSelected && item.kind === "input" && item.options.length > 0) {
|
|
416
|
+
return [main, ink4(` options: ${item.options.join(" \xB7 ")}`)];
|
|
417
|
+
}
|
|
418
|
+
return [main];
|
|
419
|
+
});
|
|
420
|
+
if (this.items.length > maxVisible)
|
|
421
|
+
lines.push(ink4(` ${this.selected + 1}/${this.items.length}`));
|
|
422
|
+
return ["", ...lines, ""];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
class HelpView {
|
|
426
|
+
group;
|
|
427
|
+
offset = 0;
|
|
428
|
+
#built = null;
|
|
429
|
+
constructor(group) {
|
|
430
|
+
this.group = group;
|
|
431
|
+
}
|
|
432
|
+
#build(width) {
|
|
433
|
+
if (this.#built)
|
|
434
|
+
return this.#built;
|
|
435
|
+
const { sections, groups } = helpCatalog();
|
|
436
|
+
const lines = [];
|
|
437
|
+
const groupAnchors = new Map;
|
|
438
|
+
const columnWidth = Math.min(38, Math.max(24, Math.floor(width * 0.4)));
|
|
439
|
+
const row = (command, description) => {
|
|
440
|
+
const left = command.length >= columnWidth ? `${command} ` : command.padEnd(columnWidth);
|
|
441
|
+
return ` ${bold(ink(left))} ${ink2(truncateToWidth(description, Math.max(16, width - columnWidth - 4)))}`;
|
|
442
|
+
};
|
|
443
|
+
for (const section of sections) {
|
|
444
|
+
lines.push(`${accent("\u25C7")} ${bold(ink(section.title))} ${ink3(`\u2014 ${section.subtitle}`)}`);
|
|
445
|
+
for (const command of section.commands)
|
|
446
|
+
lines.push(row(command.command, command.description));
|
|
447
|
+
lines.push("");
|
|
448
|
+
}
|
|
449
|
+
lines.push(`${accent("\u25C7")} ${bold(ink("command groups"))} ${ink3("\u2014 every `rig <group>` surface")}`);
|
|
450
|
+
lines.push("");
|
|
451
|
+
for (const group of groups) {
|
|
452
|
+
groupAnchors.set(group.name, lines.length);
|
|
453
|
+
lines.push(`${accentDim("\u258D")}${bold(ink(`rig ${group.name}`))} ${ink3(`\u2014 ${group.summary}`)}`);
|
|
454
|
+
for (const usage of group.usage)
|
|
455
|
+
lines.push(` ${cyan(usage)}`);
|
|
456
|
+
for (const command of group.commands)
|
|
457
|
+
lines.push(row(command.command, command.description));
|
|
458
|
+
if (group.examples?.length) {
|
|
459
|
+
for (const example of group.examples)
|
|
460
|
+
lines.push(` ${ink4("$")} ${ink2(example)}`);
|
|
461
|
+
}
|
|
462
|
+
if (group.next?.length) {
|
|
463
|
+
for (const next of group.next)
|
|
464
|
+
lines.push(` ${accent("\u203A")} ${ink3(next)}`);
|
|
465
|
+
}
|
|
466
|
+
lines.push("");
|
|
467
|
+
}
|
|
468
|
+
this.#built = { lines, groupAnchors };
|
|
469
|
+
if (this.group) {
|
|
470
|
+
const anchor = groupAnchors.get(this.group);
|
|
471
|
+
if (anchor !== undefined)
|
|
472
|
+
this.offset = anchor;
|
|
473
|
+
}
|
|
474
|
+
return this.#built;
|
|
475
|
+
}
|
|
476
|
+
scroll(delta) {
|
|
477
|
+
if (!this.#built)
|
|
478
|
+
return;
|
|
479
|
+
const max = Math.max(0, this.#built.lines.length - 10);
|
|
480
|
+
this.offset = Math.max(0, Math.min(max, this.offset + delta));
|
|
481
|
+
}
|
|
482
|
+
invalidate() {
|
|
483
|
+
this.#built = null;
|
|
484
|
+
}
|
|
485
|
+
render(width) {
|
|
486
|
+
const { lines } = this.#build(width);
|
|
487
|
+
const viewport = Math.max(10, (process.stdout.rows ?? 30) - 8);
|
|
488
|
+
const slice = lines.slice(this.offset, this.offset + viewport);
|
|
489
|
+
const more = this.offset + viewport < lines.length;
|
|
490
|
+
return [
|
|
491
|
+
"",
|
|
492
|
+
...slice.map((line) => truncateToWidth(` ${line}`, Math.max(10, width - 1))),
|
|
493
|
+
more ? ink4(` \u2026 ${lines.length - this.offset - viewport} more (\u2193)`) : ""
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// packages/rig-extension/src/workflow/projections.ts
|
|
499
|
+
var RIG_WORKFLOW_STARTED = "rig.workflow.started";
|
|
500
|
+
var RIG_WORKFLOW_TARGET_SELECTED = "rig.workflow.target.selected";
|
|
501
|
+
var RIG_WORKFLOW_TASK_SELECTED = "rig.workflow.task.selected";
|
|
502
|
+
var RIG_WORKFLOW_STATUS_CHANGED = "rig.workflow.status.changed";
|
|
503
|
+
var RIG_WORKFLOW_OPERATOR_NOTE = "rig.workflow.operator.note";
|
|
504
|
+
var RIG_WORKFLOW_INBOX_REQUESTED = "rig.workflow.inbox.requested";
|
|
505
|
+
var RIG_WORKFLOW_INBOX_RESOLVED = "rig.workflow.inbox.resolved";
|
|
506
|
+
var EMPTY_PROJECTION = {
|
|
507
|
+
started: null,
|
|
508
|
+
target: null,
|
|
509
|
+
task: null,
|
|
510
|
+
status: null,
|
|
511
|
+
notes: [],
|
|
512
|
+
inbox: [],
|
|
513
|
+
resolvedInbox: [],
|
|
514
|
+
updatedAt: null
|
|
515
|
+
};
|
|
516
|
+
function asRecord(value) {
|
|
517
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
518
|
+
}
|
|
519
|
+
function optionalString(value) {
|
|
520
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
521
|
+
}
|
|
522
|
+
function requiredString(value) {
|
|
523
|
+
const text = optionalString(value);
|
|
524
|
+
return text ?? null;
|
|
525
|
+
}
|
|
526
|
+
function timestamp(value) {
|
|
527
|
+
const text = requiredString(value);
|
|
528
|
+
if (!text)
|
|
529
|
+
return null;
|
|
530
|
+
const millis = Date.parse(text);
|
|
531
|
+
return Number.isFinite(millis) ? text : null;
|
|
532
|
+
}
|
|
533
|
+
function newer(left, right) {
|
|
534
|
+
if (!right)
|
|
535
|
+
return false;
|
|
536
|
+
if (!left)
|
|
537
|
+
return true;
|
|
538
|
+
return Date.parse(right) >= Date.parse(left);
|
|
539
|
+
}
|
|
540
|
+
function isWorkflowTarget(value) {
|
|
541
|
+
return value === "local" || value === "remote";
|
|
542
|
+
}
|
|
543
|
+
function isWorkflowStatus(value) {
|
|
544
|
+
return value === "starting" || value === "running" || value === "waiting-approval" || value === "waiting-input" || value === "completed" || value === "failed" || value === "stopped";
|
|
545
|
+
}
|
|
546
|
+
function isInboxKind(value) {
|
|
547
|
+
return value === "approval" || value === "input";
|
|
548
|
+
}
|
|
549
|
+
function isInboxDecision(value) {
|
|
550
|
+
return value === "approved" || value === "rejected" || value === "answered";
|
|
551
|
+
}
|
|
552
|
+
function parseOwner(value) {
|
|
553
|
+
const record = asRecord(value);
|
|
554
|
+
if (!record)
|
|
555
|
+
return null;
|
|
556
|
+
const githubUserId = requiredString(record.githubUserId);
|
|
557
|
+
const login = requiredString(record.login);
|
|
558
|
+
const namespaceKey = requiredString(record.namespaceKey);
|
|
559
|
+
return githubUserId && login && namespaceKey ? { githubUserId, login, namespaceKey } : null;
|
|
560
|
+
}
|
|
561
|
+
function parseStringArray(value) {
|
|
562
|
+
if (!Array.isArray(value))
|
|
563
|
+
return;
|
|
564
|
+
const options = value.map(optionalString).filter((option) => option !== undefined);
|
|
565
|
+
return options.length > 0 ? options : undefined;
|
|
566
|
+
}
|
|
567
|
+
function hasOwn(record, key) {
|
|
568
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
569
|
+
}
|
|
570
|
+
function entryUpdatedAt(entry) {
|
|
571
|
+
if ("createdAt" in entry)
|
|
572
|
+
return entry.createdAt;
|
|
573
|
+
if ("selectedAt" in entry)
|
|
574
|
+
return entry.selectedAt;
|
|
575
|
+
if ("changedAt" in entry)
|
|
576
|
+
return entry.changedAt;
|
|
577
|
+
if ("notedAt" in entry)
|
|
578
|
+
return entry.notedAt;
|
|
579
|
+
if ("requestedAt" in entry)
|
|
580
|
+
return entry.requestedAt;
|
|
581
|
+
return entry.resolvedAt;
|
|
582
|
+
}
|
|
583
|
+
function createWorkflowTaskSelected(input) {
|
|
584
|
+
return {
|
|
585
|
+
schemaVersion: 1,
|
|
586
|
+
taskId: input.taskId,
|
|
587
|
+
...input.title ? { title: input.title } : {},
|
|
588
|
+
selectedAt: input.selectedAt ?? new Date().toISOString()
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function createWorkflowStatusChanged(input) {
|
|
592
|
+
return {
|
|
593
|
+
schemaVersion: 1,
|
|
594
|
+
status: input.status,
|
|
595
|
+
...input.detail ? { detail: input.detail } : {},
|
|
596
|
+
changedAt: input.changedAt ?? new Date().toISOString()
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function createWorkflowOperatorNote(input) {
|
|
600
|
+
return {
|
|
601
|
+
schemaVersion: 1,
|
|
602
|
+
...input.noteId ? { noteId: input.noteId } : {},
|
|
603
|
+
note: input.note,
|
|
604
|
+
notedAt: input.notedAt ?? new Date().toISOString()
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function createWorkflowInboxRequested(input) {
|
|
608
|
+
return {
|
|
609
|
+
schemaVersion: 1,
|
|
610
|
+
requestId: input.requestId,
|
|
611
|
+
kind: input.kind,
|
|
612
|
+
title: input.title,
|
|
613
|
+
...input.body ? { body: input.body } : {},
|
|
614
|
+
...input.options && input.options.length > 0 ? { options: input.options } : {},
|
|
615
|
+
requestedAt: input.requestedAt ?? new Date().toISOString()
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function createWorkflowInboxResolved(input) {
|
|
619
|
+
const base = {
|
|
620
|
+
schemaVersion: 1,
|
|
621
|
+
requestId: input.requestId,
|
|
622
|
+
decision: input.decision,
|
|
623
|
+
resolvedAt: input.resolvedAt ?? new Date().toISOString()
|
|
624
|
+
};
|
|
625
|
+
return hasOwn(input, "answer") ? { ...base, answer: input.answer } : base;
|
|
626
|
+
}
|
|
627
|
+
function parseWorkflowStarted(data) {
|
|
628
|
+
const record = asRecord(data);
|
|
629
|
+
if (!record || record.schemaVersion !== 1)
|
|
630
|
+
return null;
|
|
631
|
+
const workflowId = requiredString(record.workflowId);
|
|
632
|
+
const target = optionalString(record.target);
|
|
633
|
+
const selectedRepo = requiredString(record.selectedRepo);
|
|
634
|
+
const owner = parseOwner(record.owner);
|
|
635
|
+
const createdAt = timestamp(record.createdAt);
|
|
636
|
+
if (!workflowId || !isWorkflowTarget(target) || !selectedRepo || !owner || !createdAt)
|
|
637
|
+
return null;
|
|
638
|
+
return {
|
|
639
|
+
schemaVersion: 1,
|
|
640
|
+
workflowId,
|
|
641
|
+
target,
|
|
642
|
+
selectedRepo,
|
|
643
|
+
owner,
|
|
644
|
+
createdAt
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function parseWorkflowTargetSelected(data) {
|
|
648
|
+
const record = asRecord(data);
|
|
649
|
+
if (!record || record.schemaVersion !== 1)
|
|
650
|
+
return null;
|
|
651
|
+
const target = optionalString(record.target);
|
|
652
|
+
const selectedAt = timestamp(record.selectedAt);
|
|
653
|
+
if (!isWorkflowTarget(target) || !selectedAt)
|
|
654
|
+
return null;
|
|
655
|
+
const reason = optionalString(record.reason);
|
|
656
|
+
return {
|
|
657
|
+
schemaVersion: 1,
|
|
658
|
+
target,
|
|
659
|
+
...reason ? { reason } : {},
|
|
660
|
+
selectedAt
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function parseWorkflowTaskSelected(data) {
|
|
664
|
+
const record = asRecord(data);
|
|
665
|
+
if (!record || record.schemaVersion !== 1)
|
|
666
|
+
return null;
|
|
667
|
+
const taskId = requiredString(record.taskId);
|
|
668
|
+
const selectedAt = timestamp(record.selectedAt);
|
|
669
|
+
if (!taskId || !selectedAt)
|
|
670
|
+
return null;
|
|
671
|
+
const title = optionalString(record.title);
|
|
672
|
+
return { schemaVersion: 1, taskId, ...title ? { title } : {}, selectedAt };
|
|
673
|
+
}
|
|
674
|
+
function parseWorkflowStatusChanged(data) {
|
|
675
|
+
const record = asRecord(data);
|
|
676
|
+
if (!record || record.schemaVersion !== 1)
|
|
677
|
+
return null;
|
|
678
|
+
const status = optionalString(record.status);
|
|
679
|
+
const changedAt = timestamp(record.changedAt);
|
|
680
|
+
if (!changedAt || !isWorkflowStatus(status))
|
|
681
|
+
return null;
|
|
682
|
+
const detail = optionalString(record.detail);
|
|
683
|
+
return { schemaVersion: 1, status, ...detail ? { detail } : {}, changedAt };
|
|
684
|
+
}
|
|
685
|
+
function parseWorkflowOperatorNote(data) {
|
|
686
|
+
const record = asRecord(data);
|
|
687
|
+
if (!record || record.schemaVersion !== 1)
|
|
688
|
+
return null;
|
|
689
|
+
const note = requiredString(record.note);
|
|
690
|
+
const notedAt = timestamp(record.notedAt);
|
|
691
|
+
if (!note || !notedAt)
|
|
692
|
+
return null;
|
|
693
|
+
const noteId = optionalString(record.noteId);
|
|
694
|
+
return { schemaVersion: 1, ...noteId ? { noteId } : {}, note, notedAt };
|
|
695
|
+
}
|
|
696
|
+
function parseWorkflowInboxRequested(data) {
|
|
697
|
+
const record = asRecord(data);
|
|
698
|
+
if (!record || record.schemaVersion !== 1)
|
|
699
|
+
return null;
|
|
700
|
+
const requestId = requiredString(record.requestId);
|
|
701
|
+
const kind = optionalString(record.kind);
|
|
702
|
+
const title = requiredString(record.title);
|
|
703
|
+
const requestedAt = timestamp(record.requestedAt);
|
|
704
|
+
if (!requestId || !isInboxKind(kind) || !title || !requestedAt)
|
|
705
|
+
return null;
|
|
706
|
+
const body = optionalString(record.body);
|
|
707
|
+
const options = parseStringArray(record.options);
|
|
708
|
+
return {
|
|
709
|
+
schemaVersion: 1,
|
|
710
|
+
requestId,
|
|
711
|
+
kind,
|
|
712
|
+
title,
|
|
713
|
+
...body ? { body } : {},
|
|
714
|
+
...options ? { options } : {},
|
|
715
|
+
requestedAt
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function parseWorkflowInboxResolved(data) {
|
|
719
|
+
const record = asRecord(data);
|
|
720
|
+
if (!record || record.schemaVersion !== 1)
|
|
721
|
+
return null;
|
|
722
|
+
const requestId = requiredString(record.requestId);
|
|
723
|
+
const decision = optionalString(record.decision);
|
|
724
|
+
const resolvedAt = timestamp(record.resolvedAt);
|
|
725
|
+
if (!requestId || !isInboxDecision(decision) || !resolvedAt)
|
|
726
|
+
return null;
|
|
727
|
+
const base = { schemaVersion: 1, requestId, decision, resolvedAt };
|
|
728
|
+
return hasOwn(record, "answer") ? { ...base, answer: record.answer } : base;
|
|
729
|
+
}
|
|
730
|
+
function collectPendingInboxRequests(entries) {
|
|
731
|
+
const requested = new Map;
|
|
732
|
+
const resolved = new Map;
|
|
733
|
+
for (const entry of entries) {
|
|
734
|
+
if (entry.type !== "custom")
|
|
735
|
+
continue;
|
|
736
|
+
if (entry.customType === RIG_WORKFLOW_INBOX_REQUESTED) {
|
|
737
|
+
const request = parseWorkflowInboxRequested(entry.data);
|
|
738
|
+
if (request && newer(requested.get(request.requestId)?.requestedAt, request.requestedAt))
|
|
739
|
+
requested.set(request.requestId, request);
|
|
740
|
+
} else if (entry.customType === RIG_WORKFLOW_INBOX_RESOLVED) {
|
|
741
|
+
const resolution = parseWorkflowInboxResolved(entry.data);
|
|
742
|
+
if (resolution && newer(resolved.get(resolution.requestId), resolution.resolvedAt))
|
|
743
|
+
resolved.set(resolution.requestId, resolution.resolvedAt);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const [requestId, request] of requested) {
|
|
747
|
+
const resolvedAt = resolved.get(requestId);
|
|
748
|
+
if (resolvedAt && Date.parse(resolvedAt) >= Date.parse(request.requestedAt))
|
|
749
|
+
requested.delete(requestId);
|
|
750
|
+
}
|
|
751
|
+
return Array.from(requested.values()).sort((a, b) => Date.parse(b.requestedAt) - Date.parse(a.requestedAt));
|
|
752
|
+
}
|
|
753
|
+
function collectResolvedInboxRequests(entries) {
|
|
754
|
+
const requested = new Map;
|
|
755
|
+
const resolved = new Map;
|
|
756
|
+
for (const entry of entries) {
|
|
757
|
+
if (entry.type !== "custom")
|
|
758
|
+
continue;
|
|
759
|
+
if (entry.customType === RIG_WORKFLOW_INBOX_REQUESTED) {
|
|
760
|
+
const request = parseWorkflowInboxRequested(entry.data);
|
|
761
|
+
if (request && newer(requested.get(request.requestId), request.requestedAt))
|
|
762
|
+
requested.set(request.requestId, request.requestedAt);
|
|
763
|
+
} else if (entry.customType === RIG_WORKFLOW_INBOX_RESOLVED) {
|
|
764
|
+
const resolution = parseWorkflowInboxResolved(entry.data);
|
|
765
|
+
if (resolution && newer(resolved.get(resolution.requestId)?.resolvedAt, resolution.resolvedAt)) {
|
|
766
|
+
resolved.set(resolution.requestId, resolution);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const [requestId, resolution] of resolved) {
|
|
771
|
+
const requestedAt = requested.get(requestId);
|
|
772
|
+
if (requestedAt && Date.parse(requestedAt) > Date.parse(resolution.resolvedAt))
|
|
773
|
+
resolved.delete(requestId);
|
|
774
|
+
}
|
|
775
|
+
return Array.from(resolved.values()).sort((a, b) => Date.parse(b.resolvedAt) - Date.parse(a.resolvedAt));
|
|
776
|
+
}
|
|
777
|
+
function projectCollabWorkflowMarker(collab) {
|
|
778
|
+
const status = createWorkflowStatusChanged({
|
|
779
|
+
status: collab.stale ? "stopped" : "running",
|
|
780
|
+
detail: collab.stale ? "Stale OMP collab session discovered from the sanctioned collab registry." : "Live OMP collab session discovered from the sanctioned collab registry.",
|
|
781
|
+
changedAt: collab.updatedAt
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
started: null,
|
|
785
|
+
target: null,
|
|
786
|
+
task: null,
|
|
787
|
+
status,
|
|
788
|
+
notes: [],
|
|
789
|
+
inbox: [],
|
|
790
|
+
resolvedInbox: [],
|
|
791
|
+
updatedAt: newer(collab.startedAt, collab.updatedAt) ? collab.updatedAt : collab.startedAt
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function projectWorkflowEntries(entries) {
|
|
795
|
+
let started = null;
|
|
796
|
+
let target = null;
|
|
797
|
+
let task = null;
|
|
798
|
+
let status = null;
|
|
799
|
+
const notes = [];
|
|
800
|
+
let updatedAt = null;
|
|
801
|
+
for (const entry of entries) {
|
|
802
|
+
if (entry.type !== "custom")
|
|
803
|
+
continue;
|
|
804
|
+
if (entry.customType === RIG_WORKFLOW_STARTED) {
|
|
805
|
+
const parsed = parseWorkflowStarted(entry.data);
|
|
806
|
+
if (parsed && newer(started?.createdAt, parsed.createdAt))
|
|
807
|
+
started = parsed;
|
|
808
|
+
} else if (entry.customType === RIG_WORKFLOW_TARGET_SELECTED) {
|
|
809
|
+
const parsed = parseWorkflowTargetSelected(entry.data);
|
|
810
|
+
if (parsed && newer(target?.selectedAt, parsed.selectedAt))
|
|
811
|
+
target = parsed;
|
|
812
|
+
} else if (entry.customType === RIG_WORKFLOW_TASK_SELECTED) {
|
|
813
|
+
const parsed = parseWorkflowTaskSelected(entry.data);
|
|
814
|
+
if (parsed && newer(task?.selectedAt, parsed.selectedAt))
|
|
815
|
+
task = parsed;
|
|
816
|
+
} else if (entry.customType === RIG_WORKFLOW_STATUS_CHANGED) {
|
|
817
|
+
const parsed = parseWorkflowStatusChanged(entry.data);
|
|
818
|
+
if (parsed && newer(status?.changedAt, parsed.changedAt))
|
|
819
|
+
status = parsed;
|
|
820
|
+
} else if (entry.customType === RIG_WORKFLOW_OPERATOR_NOTE) {
|
|
821
|
+
const parsed = parseWorkflowOperatorNote(entry.data);
|
|
822
|
+
if (parsed)
|
|
823
|
+
notes.push(parsed);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
notes.sort((a, b) => Date.parse(b.notedAt) - Date.parse(a.notedAt));
|
|
827
|
+
for (const value of [started, target, task, status, ...notes].map((entry) => entry ? entryUpdatedAt(entry) : null)) {
|
|
828
|
+
if (newer(updatedAt, value))
|
|
829
|
+
updatedAt = value;
|
|
830
|
+
}
|
|
831
|
+
const inbox = collectPendingInboxRequests(entries);
|
|
832
|
+
const resolvedInbox = collectResolvedInboxRequests(entries);
|
|
833
|
+
for (const entry of [...inbox, ...resolvedInbox]) {
|
|
834
|
+
if (newer(updatedAt, entryUpdatedAt(entry)))
|
|
835
|
+
updatedAt = entryUpdatedAt(entry);
|
|
836
|
+
}
|
|
837
|
+
if (!started && !target && !task && !status && notes.length === 0 && inbox.length === 0 && resolvedInbox.length === 0 && !updatedAt)
|
|
838
|
+
return EMPTY_PROJECTION;
|
|
839
|
+
return { started, target, task, status, notes, inbox, resolvedInbox, updatedAt };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// packages/rig-extension/src/workflow/identity.ts
|
|
843
|
+
function stringField(value) {
|
|
844
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
845
|
+
}
|
|
846
|
+
function numericStringField(value) {
|
|
847
|
+
return typeof value === "number" && Number.isInteger(value) ? String(value) : stringField(value);
|
|
848
|
+
}
|
|
849
|
+
function asRecord2(value) {
|
|
850
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
851
|
+
}
|
|
852
|
+
function customEntries(entries) {
|
|
853
|
+
const customs = [];
|
|
854
|
+
for (const entry of entries) {
|
|
855
|
+
if (entry.type === "custom")
|
|
856
|
+
customs.push(entry);
|
|
857
|
+
}
|
|
858
|
+
return customs;
|
|
859
|
+
}
|
|
860
|
+
function candidateFromStartedProjection(workflow) {
|
|
861
|
+
const started = workflow.started;
|
|
862
|
+
if (!started)
|
|
863
|
+
return null;
|
|
864
|
+
return {
|
|
865
|
+
selectedRepo: started.selectedRepo,
|
|
866
|
+
githubUserId: started.owner.githubUserId,
|
|
867
|
+
login: started.owner.login,
|
|
868
|
+
namespaceKey: started.owner.namespaceKey,
|
|
869
|
+
source: "workflow-metadata"
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
function candidateFromStartedData(data) {
|
|
873
|
+
const record = asRecord2(data);
|
|
874
|
+
if (!record)
|
|
875
|
+
return null;
|
|
876
|
+
const owner = asRecord2(record.owner);
|
|
877
|
+
const selectedRepo = stringField(record.selectedRepo);
|
|
878
|
+
const githubUserId = stringField(owner?.githubUserId) ?? numericStringField(record.githubUserId) ?? numericStringField(record.userId);
|
|
879
|
+
const login = stringField(owner?.login) ?? stringField(record.login);
|
|
880
|
+
const namespaceKey = stringField(owner?.namespaceKey) ?? stringField(record.namespaceKey) ?? stringField(record.userNamespaceKey);
|
|
881
|
+
if (!selectedRepo && !githubUserId && !login && !namespaceKey)
|
|
882
|
+
return null;
|
|
883
|
+
return { selectedRepo, githubUserId, login, namespaceKey, source: "workflow-metadata" };
|
|
884
|
+
}
|
|
885
|
+
function workflowIdentityCandidate(entries) {
|
|
886
|
+
const customs = customEntries(entries);
|
|
887
|
+
const projected = candidateFromStartedProjection(projectWorkflowEntries(customs));
|
|
888
|
+
if (projected)
|
|
889
|
+
return projected;
|
|
890
|
+
for (let index = customs.length - 1;index >= 0; index -= 1) {
|
|
891
|
+
const entry = customs[index];
|
|
892
|
+
if (entry?.customType !== RIG_WORKFLOW_STARTED)
|
|
893
|
+
continue;
|
|
894
|
+
const candidate = candidateFromStartedData(entry.data);
|
|
895
|
+
if (candidate)
|
|
896
|
+
return candidate;
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
function ompGitHubIdentityCandidate(ctx) {
|
|
901
|
+
const account = asRecord2(ctx.modelRegistry?.authStorage?.getOAuthAccountIdentity?.("github-copilot", ctx.sessionManager.getSessionId()));
|
|
902
|
+
if (!account)
|
|
903
|
+
return null;
|
|
904
|
+
const githubUserId = stringField(account.accountId);
|
|
905
|
+
const login = stringField(account.login) ?? stringField(account.email);
|
|
906
|
+
if (!githubUserId && !login)
|
|
907
|
+
return null;
|
|
908
|
+
return {
|
|
909
|
+
githubUserId,
|
|
910
|
+
login,
|
|
911
|
+
namespaceKey: githubUserId ? `gh:${githubUserId}` : undefined,
|
|
912
|
+
source: "omp-github-copilot-auth"
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function envField(name) {
|
|
916
|
+
return stringField(process.env[name]);
|
|
917
|
+
}
|
|
918
|
+
function envIdentityCandidate() {
|
|
919
|
+
const selectedRepo = envField("RIG_SELECTED_REPO");
|
|
920
|
+
const githubUserId = envField("RIG_GITHUB_USER_ID");
|
|
921
|
+
const login = envField("RIG_GITHUB_LOGIN");
|
|
922
|
+
const namespaceKey = envField("RIG_GITHUB_NAMESPACE_KEY") ?? (githubUserId ? `gh:${githubUserId}` : undefined);
|
|
923
|
+
if (!selectedRepo && !githubUserId && !login && !namespaceKey)
|
|
924
|
+
return null;
|
|
925
|
+
return {
|
|
926
|
+
selectedRepo,
|
|
927
|
+
githubUserId,
|
|
928
|
+
login,
|
|
929
|
+
namespaceKey,
|
|
930
|
+
source: "rig-public-identity-env"
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
function mergeSource(sources, source) {
|
|
934
|
+
if (source && !sources.includes(source))
|
|
935
|
+
sources.push(source);
|
|
936
|
+
}
|
|
937
|
+
function completeIdentity(candidate, sources) {
|
|
938
|
+
const selectedRepo = stringField(candidate.selectedRepo);
|
|
939
|
+
const githubUserId = stringField(candidate.githubUserId);
|
|
940
|
+
const login = stringField(candidate.login);
|
|
941
|
+
const namespaceKey = stringField(candidate.namespaceKey);
|
|
942
|
+
if (!selectedRepo || !githubUserId || !login || !namespaceKey)
|
|
943
|
+
return null;
|
|
944
|
+
const uniqueSources = [];
|
|
945
|
+
for (const value of sources) {
|
|
946
|
+
if (!uniqueSources.includes(value))
|
|
947
|
+
uniqueSources.push(value);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
selectedRepo,
|
|
951
|
+
owner: { githubUserId, login, namespaceKey },
|
|
952
|
+
source: uniqueSources.join("+")
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function resolveRigIdentity(ctx) {
|
|
956
|
+
const workflow = workflowIdentityCandidate(ctx.sessionManager.getBranch());
|
|
957
|
+
const omp = ompGitHubIdentityCandidate(ctx);
|
|
958
|
+
const env = envIdentityCandidate();
|
|
959
|
+
const sources = [];
|
|
960
|
+
const selectedRepo = workflow?.selectedRepo ?? env?.selectedRepo;
|
|
961
|
+
const githubUserId = workflow?.githubUserId ?? omp?.githubUserId ?? env?.githubUserId;
|
|
962
|
+
const login = workflow?.login ?? omp?.login ?? env?.login;
|
|
963
|
+
const namespaceKey = workflow?.namespaceKey ?? omp?.namespaceKey ?? env?.namespaceKey;
|
|
964
|
+
if (workflow && (workflow.selectedRepo || workflow.githubUserId || workflow.login || workflow.namespaceKey))
|
|
965
|
+
mergeSource(sources, workflow.source);
|
|
966
|
+
if (omp && (!workflow?.githubUserId || !workflow?.login || !workflow?.namespaceKey))
|
|
967
|
+
mergeSource(sources, omp.source);
|
|
968
|
+
if (env && (!workflow?.selectedRepo && env.selectedRepo || !workflow?.githubUserId && !omp?.githubUserId && env.githubUserId || !workflow?.login && !omp?.login && env.login || !workflow?.namespaceKey && !omp?.namespaceKey && env.namespaceKey))
|
|
969
|
+
mergeSource(sources, env.source);
|
|
970
|
+
return completeIdentity({ selectedRepo, githubUserId, login, namespaceKey, source: sources.join("+") }, sources);
|
|
971
|
+
}
|
|
972
|
+
function resolveRigIdentityFilter(ctx) {
|
|
973
|
+
const identity = resolveRigIdentity(ctx);
|
|
974
|
+
if (!identity)
|
|
975
|
+
return null;
|
|
976
|
+
return {
|
|
977
|
+
cwd: ctx.sessionManager.getCwd(),
|
|
978
|
+
selectedRepo: identity.selectedRepo,
|
|
979
|
+
githubUserId: identity.owner.githubUserId,
|
|
980
|
+
namespaceKey: identity.owner.namespaceKey
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// packages/rig-extension/src/extension.ts
|
|
985
|
+
function isRigScreen(value) {
|
|
986
|
+
return value === "cockpit" || value === "server" || value === "tasks" || value === "task-detail" || value === "runs" || value === "inbox" || value === "doctor" || value === "setup";
|
|
987
|
+
}
|
|
988
|
+
var RIG_EXTENSION_BRIDGE_GLOBAL = "__RIG_EXTENSION_BRIDGE__";
|
|
989
|
+
function projectRootFromContext(ctx) {
|
|
990
|
+
return ctx.cwd || ctx.sessionManager.getCwd?.() || process.cwd();
|
|
991
|
+
}
|
|
992
|
+
function connectionStatePath(projectRoot) {
|
|
993
|
+
return resolve(resolveAuthorityPaths(projectRoot).stateDir, "connection.json");
|
|
994
|
+
}
|
|
995
|
+
function readJsonRecord(path) {
|
|
996
|
+
if (!existsSync(path))
|
|
997
|
+
return null;
|
|
998
|
+
try {
|
|
999
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
1000
|
+
return isRecord(parsed) ? parsed : null;
|
|
1001
|
+
} catch {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function persistSelectedTarget(projectRoot, alias) {
|
|
1006
|
+
const path = connectionStatePath(projectRoot);
|
|
1007
|
+
const previous = readJsonRecord(path) ?? {};
|
|
1008
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1009
|
+
writeFileSync(path, `${JSON.stringify({ ...previous, selected: alias }, null, 2)}
|
|
1010
|
+
`, "utf-8");
|
|
1011
|
+
if (alias === "local") {
|
|
1012
|
+
delete process.env.RIG_REMOTE_ALIAS;
|
|
1013
|
+
} else {
|
|
1014
|
+
process.env.RIG_REMOTE_ALIAS = alias;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function remoteTargetFromEndpoint(remote, projectRoot) {
|
|
1018
|
+
return {
|
|
1019
|
+
alias: remote.alias,
|
|
1020
|
+
kind: "remote",
|
|
1021
|
+
baseUrl: `${remote.host}:${remote.port}`,
|
|
1022
|
+
projectRoot,
|
|
1023
|
+
status: "ready",
|
|
1024
|
+
taskSource: "configured source",
|
|
1025
|
+
detail: remote.lastConnected ? `last connected ${remote.lastConnected}` : "saved remote alias"
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
async function listServerTargets(ctx, bridge) {
|
|
1029
|
+
if (bridge?.listServerTargets)
|
|
1030
|
+
return bridge.listServerTargets();
|
|
1031
|
+
const projectRoot = projectRootFromContext(ctx);
|
|
1032
|
+
const current = normalizeSpawnTargetState(await bridge?.readServerState?.() ?? null);
|
|
1033
|
+
const remotes = listManagedRemoteEndpoints(undefined, projectRoot).map((remote) => remoteTargetFromEndpoint(remote, projectRoot));
|
|
1034
|
+
const targets = [
|
|
1035
|
+
{
|
|
1036
|
+
alias: "local",
|
|
1037
|
+
kind: "local",
|
|
1038
|
+
projectRoot,
|
|
1039
|
+
status: "ready",
|
|
1040
|
+
taskSource: current?.kind === "local" ? current.taskSource ?? "configured source" : "configured source",
|
|
1041
|
+
detail: "this checkout"
|
|
1042
|
+
},
|
|
1043
|
+
...remotes
|
|
1044
|
+
];
|
|
1045
|
+
return targets.map((target) => target.alias === current?.alias ? { ...target, ...current, status: "ready" } : target);
|
|
1046
|
+
}
|
|
1047
|
+
async function selectServerTarget(ctx, bridge, alias) {
|
|
1048
|
+
if (bridge?.selectServerTarget)
|
|
1049
|
+
return bridge.selectServerTarget(alias);
|
|
1050
|
+
const projectRoot = projectRootFromContext(ctx);
|
|
1051
|
+
const targets = await listServerTargets(ctx, bridge);
|
|
1052
|
+
const target = targets.find((entry) => entry.alias === alias);
|
|
1053
|
+
if (!target)
|
|
1054
|
+
throw new Error(`Unknown Rig server target: ${alias}`);
|
|
1055
|
+
persistSelectedTarget(projectRoot, alias);
|
|
1056
|
+
return target;
|
|
1057
|
+
}
|
|
1058
|
+
async function addRemoteTarget(ctx, bridge) {
|
|
1059
|
+
const alias = (await ctx.ui.editor("Rig remote alias", "prod"))?.trim();
|
|
1060
|
+
if (!alias)
|
|
1061
|
+
return null;
|
|
1062
|
+
const host = (await ctx.ui.editor("Rig remote host", "where.rig-does.work"))?.trim();
|
|
1063
|
+
if (!host)
|
|
1064
|
+
return null;
|
|
1065
|
+
const portRaw = (await ctx.ui.editor("Rig remote port", "7890"))?.trim() || "7890";
|
|
1066
|
+
const port = Number.parseInt(portRaw, 10);
|
|
1067
|
+
if (!Number.isFinite(port) || port <= 0 || port > 65535)
|
|
1068
|
+
throw new Error(`Invalid Rig remote port: ${portRaw}`);
|
|
1069
|
+
const token = (await ctx.ui.editor("Rig remote token (optional)", ""))?.trim() || null;
|
|
1070
|
+
if (bridge?.addRemoteTarget)
|
|
1071
|
+
return bridge.addRemoteTarget({ alias, host, port, token });
|
|
1072
|
+
const projectRoot = projectRootFromContext(ctx);
|
|
1073
|
+
const saved = upsertManagedRemoteEndpoint({ alias, host, port, ...token ? { token } : {} }, undefined, projectRoot);
|
|
1074
|
+
persistSelectedTarget(projectRoot, alias);
|
|
1075
|
+
return remoteTargetFromEndpoint(saved, projectRoot);
|
|
1076
|
+
}
|
|
1077
|
+
var RIG_LABELS_TO_ENSURE = [
|
|
1078
|
+
"rig:running",
|
|
1079
|
+
"rig:pr-open",
|
|
1080
|
+
"rig:ci-fixing",
|
|
1081
|
+
"rig:merging",
|
|
1082
|
+
"rig:done",
|
|
1083
|
+
"rig:needs-attention",
|
|
1084
|
+
"rig:ready",
|
|
1085
|
+
"rig:blocked",
|
|
1086
|
+
"rig:generated"
|
|
1087
|
+
];
|
|
1088
|
+
var RIG_LABEL_METADATA = {
|
|
1089
|
+
"rig:running": { color: "1d76db", description: "Rig is actively working on this issue." },
|
|
1090
|
+
"rig:pr-open": { color: "5319e7", description: "Rig opened a pull request for this issue." },
|
|
1091
|
+
"rig:ci-fixing": { color: "fbca04", description: "Rig is fixing CI or review feedback for this issue." },
|
|
1092
|
+
"rig:merging": { color: "0052cc", description: "Rig is merging the completed change for this issue." },
|
|
1093
|
+
"rig:done": { color: "0e8a16", description: "Rig completed this issue." },
|
|
1094
|
+
"rig:needs-attention": { color: "d93f0b", description: "Rig needs operator attention for this issue." },
|
|
1095
|
+
"rig:ready": { color: "0e8a16", description: "Rig issue analysis marked this issue ready." },
|
|
1096
|
+
"rig:blocked": { color: "d93f0b", description: "Rig issue analysis found blockers for this issue." },
|
|
1097
|
+
"rig:generated": { color: "c5def5", description: "Rig generated this follow-up issue." }
|
|
1098
|
+
};
|
|
1099
|
+
function cleanString(value) {
|
|
1100
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1101
|
+
}
|
|
1102
|
+
function parseRepoSlugFromRemote(remoteUrl) {
|
|
1103
|
+
const trimmed = remoteUrl.trim();
|
|
1104
|
+
const match = trimmed.match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?$/i);
|
|
1105
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
1106
|
+
}
|
|
1107
|
+
function parseRepoSlug(value) {
|
|
1108
|
+
const match = value.trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
1109
|
+
if (!match)
|
|
1110
|
+
throw new Error(`Invalid GitHub repo slug "${value}". Expected owner/repo.`);
|
|
1111
|
+
return { owner: match[1], repo: match[2], slug: `${match[1]}/${match[2]}` };
|
|
1112
|
+
}
|
|
1113
|
+
function runSyncCommand(command, input = {}) {
|
|
1114
|
+
const executable = command[0];
|
|
1115
|
+
if (!executable)
|
|
1116
|
+
throw new Error("command is required");
|
|
1117
|
+
return (input.spawn ?? spawnSync)(executable, [...command.slice(1)], {
|
|
1118
|
+
cwd: input.cwd,
|
|
1119
|
+
encoding: "utf8",
|
|
1120
|
+
timeout: input.timeoutMs ?? 1e4,
|
|
1121
|
+
env: input.env ?? process.env
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
function detectOriginRepoSlug(projectRoot, deps = {}) {
|
|
1125
|
+
const result = runSyncCommand(["git", "-C", projectRoot, "remote", "get-url", "origin"], { timeoutMs: 5000, spawn: deps.spawn });
|
|
1126
|
+
if (result.status !== 0 || result.error)
|
|
1127
|
+
return null;
|
|
1128
|
+
return parseRepoSlugFromRemote(result.stdout.trim());
|
|
1129
|
+
}
|
|
1130
|
+
function readRigConfigStatus(projectRoot) {
|
|
1131
|
+
const path = resolve(projectRoot, "rig.config.ts");
|
|
1132
|
+
if (!existsSync(path)) {
|
|
1133
|
+
return { exists: false, valid: false, path, slug: null, reason: "missing rig.config.ts" };
|
|
1134
|
+
}
|
|
1135
|
+
try {
|
|
1136
|
+
const source = readFileSync(path, "utf-8");
|
|
1137
|
+
const owner = source.match(/\bowner:\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
1138
|
+
const repoValues = [...source.matchAll(/\brepo:\s*["']([^"']+)["']/g)].map((match) => match[1]).filter(Boolean);
|
|
1139
|
+
const taskRepo = repoValues.find((value) => !value.includes("/")) ?? null;
|
|
1140
|
+
const projectRepo = repoValues.find((value) => value.includes("/")) ?? null;
|
|
1141
|
+
const githubIssues = /\bkind:\s*["']github-issues["']/.test(source);
|
|
1142
|
+
const slug = owner && taskRepo ? `${owner}/${taskRepo}` : projectRepo;
|
|
1143
|
+
if (!githubIssues || !slug) {
|
|
1144
|
+
return { exists: true, valid: false, path, slug: slug ?? null, reason: "rig.config.ts is not a GitHub Issues Rig config" };
|
|
1145
|
+
}
|
|
1146
|
+
parseRepoSlug(slug);
|
|
1147
|
+
return { exists: true, valid: true, path, slug };
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
return { exists: true, valid: false, path, slug: null, reason: notificationMessage(error) };
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function readRigConnectionStatus(projectRoot) {
|
|
1153
|
+
const stateDir = resolve(projectRoot, ".rig", "state");
|
|
1154
|
+
if (!existsSync(stateDir))
|
|
1155
|
+
return { valid: false, selected: null, project: null, reason: "missing .rig/state" };
|
|
1156
|
+
const connection = readJsonRecord(connectionStatePath(projectRoot));
|
|
1157
|
+
if (!connection)
|
|
1158
|
+
return { valid: false, selected: null, project: null, reason: "missing or invalid .rig/state/connection.json" };
|
|
1159
|
+
const selected = cleanString(connection.selected);
|
|
1160
|
+
const project = cleanString(connection.project);
|
|
1161
|
+
if (!selected)
|
|
1162
|
+
return { valid: false, selected: null, project, reason: "connection.json is missing selected placement" };
|
|
1163
|
+
if (!project)
|
|
1164
|
+
return { valid: false, selected, project: null, reason: "connection.json is missing project slug" };
|
|
1165
|
+
try {
|
|
1166
|
+
parseRepoSlug(project);
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
return { valid: false, selected, project, reason: notificationMessage(error) };
|
|
1169
|
+
}
|
|
1170
|
+
return { valid: true, selected, project };
|
|
1171
|
+
}
|
|
1172
|
+
function projectLinkStatePath(projectRoot) {
|
|
1173
|
+
return resolve(projectRoot, ".rig", "state", "project-link.json");
|
|
1174
|
+
}
|
|
1175
|
+
function writeJsonFile(path, value) {
|
|
1176
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1177
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
1178
|
+
`, "utf-8");
|
|
1179
|
+
}
|
|
1180
|
+
function ensureRigPrivateDirs(projectRoot) {
|
|
1181
|
+
mkdirSync(resolve(projectRoot, ".rig", "state"), { recursive: true });
|
|
1182
|
+
mkdirSync(resolve(projectRoot, ".rig", "logs"), { recursive: true });
|
|
1183
|
+
mkdirSync(resolve(projectRoot, ".rig", "runs"), { recursive: true });
|
|
1184
|
+
mkdirSync(resolve(projectRoot, ".rig", "tmp"), { recursive: true });
|
|
1185
|
+
mkdirSync(resolve(projectRoot, "artifacts"), { recursive: true });
|
|
1186
|
+
const taskConfigPath = resolve(projectRoot, ".rig", "task-config.json");
|
|
1187
|
+
if (!existsSync(taskConfigPath))
|
|
1188
|
+
writeFileSync(taskConfigPath, `{}
|
|
1189
|
+
`, "utf-8");
|
|
1190
|
+
}
|
|
1191
|
+
function ensureGitignoreEntries(projectRoot) {
|
|
1192
|
+
const path = resolve(projectRoot, ".gitignore");
|
|
1193
|
+
const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
1194
|
+
const lines = new Set(existing.split(/\r?\n/));
|
|
1195
|
+
const missing = [".rig/state/", ".rig/logs/", ".rig/runs/", ".rig/tmp/"].filter((entry) => !lines.has(entry));
|
|
1196
|
+
if (missing.length === 0)
|
|
1197
|
+
return;
|
|
1198
|
+
const prefix = existing.length > 0 && !existing.endsWith(`
|
|
1199
|
+
`) ? `
|
|
1200
|
+
` : "";
|
|
1201
|
+
appendFileSync(path, `${prefix}${missing.join(`
|
|
1202
|
+
`)}
|
|
1203
|
+
`, "utf-8");
|
|
1204
|
+
}
|
|
1205
|
+
function writeRigConnectionState(projectRoot, slug, placement) {
|
|
1206
|
+
const previous = readJsonRecord(connectionStatePath(projectRoot)) ?? {};
|
|
1207
|
+
writeJsonFile(connectionStatePath(projectRoot), {
|
|
1208
|
+
...previous,
|
|
1209
|
+
selected: placement.alias,
|
|
1210
|
+
project: slug,
|
|
1211
|
+
linkedAt: new Date().toISOString()
|
|
1212
|
+
});
|
|
1213
|
+
writeJsonFile(projectLinkStatePath(projectRoot), {
|
|
1214
|
+
repoSlug: slug,
|
|
1215
|
+
connection: placement.alias,
|
|
1216
|
+
linkedAt: new Date().toISOString()
|
|
1217
|
+
});
|
|
1218
|
+
if (placement.alias === "local")
|
|
1219
|
+
delete process.env.RIG_REMOTE_ALIAS;
|
|
1220
|
+
else
|
|
1221
|
+
process.env.RIG_REMOTE_ALIAS = placement.alias;
|
|
1222
|
+
}
|
|
1223
|
+
function writeRigConfig(projectRoot, slug) {
|
|
1224
|
+
const repo = parseRepoSlug(slug);
|
|
1225
|
+
writeFileSync(resolve(projectRoot, "rig.config.ts"), buildRigInitConfigSource({
|
|
1226
|
+
projectName: repo.slug,
|
|
1227
|
+
projectRepo: repo.slug,
|
|
1228
|
+
taskSource: { kind: "github-issues", owner: repo.owner, repo: repo.repo },
|
|
1229
|
+
useStandardPlugin: true
|
|
1230
|
+
}), "utf-8");
|
|
1231
|
+
}
|
|
1232
|
+
function detectGhAuth(projectRoot, slug, deps = {}) {
|
|
1233
|
+
const user = runSyncCommand(["gh", "api", "user", "--jq", ".login"], { cwd: projectRoot, timeoutMs: 5000, spawn: deps.spawn });
|
|
1234
|
+
if (user.status !== 0 || user.error || !user.stdout.trim())
|
|
1235
|
+
return null;
|
|
1236
|
+
const repo = runSyncCommand(["gh", "repo", "view", slug, "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { cwd: projectRoot, timeoutMs: 5000, spawn: deps.spawn });
|
|
1237
|
+
if (repo.status !== 0 || repo.error) {
|
|
1238
|
+
return { ok: false, source: "gh", login: user.stdout.trim(), detail: (repo.stderr || repo.stdout || "gh cannot access the selected repository").trim() };
|
|
1239
|
+
}
|
|
1240
|
+
return { ok: true, source: "gh", login: user.stdout.trim(), detail: "gh CLI authentication can access the selected repository" };
|
|
1241
|
+
}
|
|
1242
|
+
async function validateGitHubAuth(projectRoot, slug, deps = {}) {
|
|
1243
|
+
if (!slug)
|
|
1244
|
+
return { ok: false, source: "missing", detail: "GitHub repo slug is unknown" };
|
|
1245
|
+
const status = resolveGitHubAuthStatus({ projectRoot, oauthConfigured: Boolean(process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim()) });
|
|
1246
|
+
if (status.signedIn) {
|
|
1247
|
+
const store = createGitHubAuthStore(projectRoot);
|
|
1248
|
+
if (!status.selectedRepo) {
|
|
1249
|
+
store.saveSelectedRepo(slug);
|
|
1250
|
+
return { ok: true, source: "stored-token", login: status.login, detail: "stored Rig GitHub token selected for this repo", status: store.status({ oauthConfigured: status.oauthConfigured }) };
|
|
1251
|
+
}
|
|
1252
|
+
if (status.selectedRepo !== slug) {
|
|
1253
|
+
return { ok: false, source: "stored-token", login: status.login, detail: `stored GitHub token is scoped to ${status.selectedRepo}, not ${slug}`, status };
|
|
1254
|
+
}
|
|
1255
|
+
return { ok: true, source: "stored-token", login: status.login, detail: "stored Rig GitHub token is present", status };
|
|
1256
|
+
}
|
|
1257
|
+
const gh = detectGhAuth(projectRoot, slug, deps);
|
|
1258
|
+
if (gh)
|
|
1259
|
+
return gh;
|
|
1260
|
+
return { ok: false, source: "missing", detail: "Sign in with `gh auth login`, choose Setup \u2192 GitHub auth, or paste a token." };
|
|
1261
|
+
}
|
|
1262
|
+
function lightweightSetupDoctor(ctx, projectRoot) {
|
|
1263
|
+
const checks = [];
|
|
1264
|
+
checks.push({ label: "workspace", level: existsSync(projectRoot) ? "ok" : "fail", detail: projectRoot });
|
|
1265
|
+
checks.push({ label: "omp-ui", level: ctx.hasUI ? "ok" : "fail", detail: ctx.hasUI ? "interactive session" : "no UI context" });
|
|
1266
|
+
checks.push({ label: "pi-extension", level: "ok", detail: "Rig extension registered in this OMP session" });
|
|
1267
|
+
return checks;
|
|
1268
|
+
}
|
|
1269
|
+
async function detectRigStartupStatus(ctx, deps = {}) {
|
|
1270
|
+
const projectRoot = projectRootFromContext(ctx);
|
|
1271
|
+
const config = readRigConfigStatus(projectRoot);
|
|
1272
|
+
const state = readRigConnectionStatus(projectRoot);
|
|
1273
|
+
const detectedSlug = detectOriginRepoSlug(projectRoot, deps);
|
|
1274
|
+
const slug = config.slug ?? state.project ?? detectedSlug;
|
|
1275
|
+
const reasons = [];
|
|
1276
|
+
if (!detectedSlug)
|
|
1277
|
+
reasons.push("git origin does not point at a GitHub owner/repo remote");
|
|
1278
|
+
if (!config.exists || !config.valid)
|
|
1279
|
+
reasons.push(config.reason ?? "rig.config.ts is invalid");
|
|
1280
|
+
if (!state.valid)
|
|
1281
|
+
reasons.push(state.reason ?? ".rig/state/connection.json is invalid");
|
|
1282
|
+
if (config.slug && state.project && config.slug !== state.project)
|
|
1283
|
+
reasons.push(`rig.config.ts repo ${config.slug} does not match connection project ${state.project}`);
|
|
1284
|
+
if (slug && detectedSlug && slug !== detectedSlug)
|
|
1285
|
+
reasons.push(`configured repo ${slug} does not match git origin ${detectedSlug}`);
|
|
1286
|
+
const auth = await validateGitHubAuth(projectRoot, slug, deps);
|
|
1287
|
+
if (!auth.ok)
|
|
1288
|
+
reasons.push(auth.detail);
|
|
1289
|
+
const checks = lightweightSetupDoctor(ctx, projectRoot);
|
|
1290
|
+
return {
|
|
1291
|
+
configured: reasons.length === 0,
|
|
1292
|
+
projectRoot,
|
|
1293
|
+
slug,
|
|
1294
|
+
config,
|
|
1295
|
+
state,
|
|
1296
|
+
auth,
|
|
1297
|
+
checks,
|
|
1298
|
+
reasons
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function authStatePath(projectRoot) {
|
|
1302
|
+
return resolve(projectRoot, ".rig", "state", "github-auth.json");
|
|
1303
|
+
}
|
|
1304
|
+
function copyGitHubAuthToLocalProjectRoot(projectRoot) {
|
|
1305
|
+
const store = createGitHubAuthStore(projectRoot);
|
|
1306
|
+
if (resolve(store.stateFile) !== authStatePath(projectRoot))
|
|
1307
|
+
store.copyToLocalProjectRoot(projectRoot);
|
|
1308
|
+
}
|
|
1309
|
+
function readGhAuthToken(projectRoot, deps = {}) {
|
|
1310
|
+
const result = runSyncCommand(["gh", "auth", "token"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
1311
|
+
if (result.status !== 0 || result.error || !result.stdout.trim()) {
|
|
1312
|
+
throw new Error((result.stderr || result.stdout || "Could not read GitHub token from `gh auth token`. Run `gh auth login` or choose pasted-token/device auth.").trim());
|
|
1313
|
+
}
|
|
1314
|
+
return result.stdout.trim();
|
|
1315
|
+
}
|
|
1316
|
+
async function saveGitHubTokenLocally(projectRoot, token, slug, source, deps = {}) {
|
|
1317
|
+
ensureRigPrivateDirs(projectRoot);
|
|
1318
|
+
await saveGitHubTokenForProject({
|
|
1319
|
+
projectRoot,
|
|
1320
|
+
token,
|
|
1321
|
+
tokenSource: source,
|
|
1322
|
+
selectedRepo: slug,
|
|
1323
|
+
...deps.fetchUser ? { fetchUser: deps.fetchUser } : {}
|
|
1324
|
+
});
|
|
1325
|
+
copyGitHubAuthToLocalProjectRoot(projectRoot);
|
|
1326
|
+
}
|
|
1327
|
+
async function runGitHubDeviceAuth(projectRoot, slug, ctx, deps = {}) {
|
|
1328
|
+
const clientId = process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim();
|
|
1329
|
+
if (!clientId)
|
|
1330
|
+
throw new Error("GitHub device auth requires RIG_GITHUB_OAUTH_CLIENT_ID. Choose gh auth or pasted token instead.");
|
|
1331
|
+
ensureRigPrivateDirs(projectRoot);
|
|
1332
|
+
const device = await beginGitHubDeviceFlow({
|
|
1333
|
+
projectRoot,
|
|
1334
|
+
clientId,
|
|
1335
|
+
selectedRepo: slug,
|
|
1336
|
+
...deps.postForm ? { postForm: deps.postForm } : {}
|
|
1337
|
+
});
|
|
1338
|
+
const prompt = `Open ${device.verificationUri ?? "https://github.com/login/device"} and enter code ${device.userCode ?? "(missing code)"}. Press Enter here after authorizing.`;
|
|
1339
|
+
await ctx.ui.editor("GitHub device auth", prompt);
|
|
1340
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)));
|
|
1341
|
+
const timeoutMs = Number.parseInt(process.env.RIG_DEVICE_AUTH_POLL_TIMEOUT_MS ?? "", 10);
|
|
1342
|
+
const deadline = Date.now() + (Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : 5 * 60 * 1000);
|
|
1343
|
+
let intervalMs = Math.max(1000, device.intervalSeconds * 1000);
|
|
1344
|
+
do {
|
|
1345
|
+
const result = await pollGitHubDeviceFlow({
|
|
1346
|
+
projectRoot,
|
|
1347
|
+
clientId,
|
|
1348
|
+
pollId: device.pollId,
|
|
1349
|
+
selectedRepo: slug,
|
|
1350
|
+
...deps.postForm ? { postForm: deps.postForm } : {},
|
|
1351
|
+
...deps.fetchUser ? { fetchUser: deps.fetchUser } : {}
|
|
1352
|
+
});
|
|
1353
|
+
if (result.ok) {
|
|
1354
|
+
copyGitHubAuthToLocalProjectRoot(projectRoot);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
if (result.status === "error" || result.status === "expired")
|
|
1358
|
+
throw new Error(result.error ?? "GitHub device auth failed.");
|
|
1359
|
+
intervalMs = Math.max(intervalMs, (result.intervalSeconds ?? device.intervalSeconds) * 1000);
|
|
1360
|
+
await sleep(intervalMs);
|
|
1361
|
+
} while (Date.now() < deadline);
|
|
1362
|
+
throw new Error("Timed out waiting for GitHub device authorization.");
|
|
1363
|
+
}
|
|
1364
|
+
async function ensureGitHubAuthForSetup(projectRoot, slug, ctx, deps = {}) {
|
|
1365
|
+
const current = await validateGitHubAuth(projectRoot, slug, deps);
|
|
1366
|
+
const choices = [];
|
|
1367
|
+
if (current.ok && current.source === "stored-token")
|
|
1368
|
+
choices.push("Use stored Rig token");
|
|
1369
|
+
if (detectGhAuth(projectRoot, slug, deps)?.ok)
|
|
1370
|
+
choices.push("Import token from gh");
|
|
1371
|
+
if (process.env.RIG_GITHUB_OAUTH_CLIENT_ID?.trim())
|
|
1372
|
+
choices.push("GitHub device flow");
|
|
1373
|
+
choices.push("Paste token");
|
|
1374
|
+
const choice = await ctx.ui.select("GitHub auth", choices);
|
|
1375
|
+
if (!choice)
|
|
1376
|
+
throw new Error("Setup cancelled during GitHub auth.");
|
|
1377
|
+
if (choice === "Use stored Rig token") {
|
|
1378
|
+
createGitHubAuthStore(projectRoot).saveSelectedRepo(slug);
|
|
1379
|
+
copyGitHubAuthToLocalProjectRoot(projectRoot);
|
|
1380
|
+
} else if (choice === "Import token from gh") {
|
|
1381
|
+
await saveGitHubTokenLocally(projectRoot, readGhAuthToken(projectRoot, deps), slug, "manual-token", deps);
|
|
1382
|
+
} else if (choice === "GitHub device flow") {
|
|
1383
|
+
await runGitHubDeviceAuth(projectRoot, slug, ctx, deps);
|
|
1384
|
+
} else {
|
|
1385
|
+
const token = (await ctx.ui.editor("GitHub token", ""))?.trim();
|
|
1386
|
+
if (!token)
|
|
1387
|
+
throw new Error("GitHub token is required.");
|
|
1388
|
+
await saveGitHubTokenLocally(projectRoot, token, slug, "manual-token", deps);
|
|
1389
|
+
}
|
|
1390
|
+
return validateGitHubAuth(projectRoot, slug, deps);
|
|
1391
|
+
}
|
|
1392
|
+
async function ensureGitHubLabels(input) {
|
|
1393
|
+
const repo = parseRepoSlug(input.slug);
|
|
1394
|
+
const token = input.token?.trim() || createGitHubAuthStore(input.projectRoot).readToken();
|
|
1395
|
+
if (token) {
|
|
1396
|
+
const fetchLabels = input.deps?.fetch ?? fetch;
|
|
1397
|
+
for (const name of RIG_LABELS_TO_ENSURE) {
|
|
1398
|
+
const metadata = RIG_LABEL_METADATA[name];
|
|
1399
|
+
const response = await fetchLabels(`https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/labels`, {
|
|
1400
|
+
method: "POST",
|
|
1401
|
+
headers: {
|
|
1402
|
+
accept: "application/vnd.github+json",
|
|
1403
|
+
authorization: `Bearer ${token}`,
|
|
1404
|
+
"content-type": "application/json",
|
|
1405
|
+
"user-agent": "rig"
|
|
1406
|
+
},
|
|
1407
|
+
body: JSON.stringify({ name, color: metadata.color, description: metadata.description })
|
|
1408
|
+
});
|
|
1409
|
+
if (response.ok)
|
|
1410
|
+
continue;
|
|
1411
|
+
const text = await response.text().catch(() => "");
|
|
1412
|
+
if (response.status === 422 && /already_exists|already exists|exists/i.test(text))
|
|
1413
|
+
continue;
|
|
1414
|
+
throw new Error(`Could not create GitHub label ${name}: ${response.status} ${text || response.statusText}`);
|
|
1415
|
+
}
|
|
1416
|
+
return { ok: true, method: "api", labels: RIG_LABELS_TO_ENSURE };
|
|
1417
|
+
}
|
|
1418
|
+
const gh = detectGhAuth(input.projectRoot, input.slug, input.deps);
|
|
1419
|
+
if (!gh?.ok)
|
|
1420
|
+
throw new Error("GitHub labels require a stored Rig token or gh auth. Run Setup \u2192 GitHub auth, paste a token, or run `gh auth login`.");
|
|
1421
|
+
for (const name of RIG_LABELS_TO_ENSURE) {
|
|
1422
|
+
const metadata = RIG_LABEL_METADATA[name];
|
|
1423
|
+
const result = runSyncCommand(["gh", "label", "create", name, "--repo", input.slug, "--color", metadata.color, "--description", metadata.description, "--force"], { cwd: input.projectRoot, timeoutMs: 1e4, spawn: input.deps?.spawn });
|
|
1424
|
+
if (result.status !== 0 || result.error)
|
|
1425
|
+
throw new Error(`gh label create ${name} failed: ${(result.stderr || result.stdout || result.error?.message || "unknown error").trim()}`);
|
|
1426
|
+
}
|
|
1427
|
+
return { ok: true, method: "gh", labels: RIG_LABELS_TO_ENSURE };
|
|
1428
|
+
}
|
|
1429
|
+
function piListContainsRigExtension(output) {
|
|
1430
|
+
return output.split(/\r?\n/).some((line) => line.includes("@h-rig/pi-rig") || /(?:^|[\\/])packages[\\/]pi-rig(?:$|\s)/.test(line));
|
|
1431
|
+
}
|
|
1432
|
+
function splitInstallCommand(value) {
|
|
1433
|
+
return value.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
1434
|
+
}
|
|
1435
|
+
function ensurePiRigInstalledForSetup(projectRoot, deps = {}) {
|
|
1436
|
+
if (process.env.RIG_TEST_FAKE_PI_INSTALL === "1")
|
|
1437
|
+
return { ok: true, detail: "fake-pi" };
|
|
1438
|
+
let version = runSyncCommand(["pi", "--version"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
1439
|
+
if (version.status !== 0 || version.error) {
|
|
1440
|
+
const installCommand = process.env.RIG_PI_INSTALL_COMMAND?.trim();
|
|
1441
|
+
if (!installCommand)
|
|
1442
|
+
throw new Error(`Pi/OMP is not available: ${(version.stderr || version.stdout || version.error?.message || "pi --version failed").trim()}. Install Pi/OMP or set RIG_PI_INSTALL_COMMAND.`);
|
|
1443
|
+
const parts = splitInstallCommand(installCommand);
|
|
1444
|
+
if (parts.length === 0)
|
|
1445
|
+
throw new Error("RIG_PI_INSTALL_COMMAND is empty.");
|
|
1446
|
+
const install = runSyncCommand(parts, { cwd: projectRoot, timeoutMs: 120000, spawn: deps.spawn });
|
|
1447
|
+
if (install.status !== 0 || install.error)
|
|
1448
|
+
throw new Error(`Pi/OMP install command failed: ${(install.stderr || install.stdout || install.error?.message || "unknown error").trim()}`);
|
|
1449
|
+
version = runSyncCommand(["pi", "--version"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
1450
|
+
if (version.status !== 0 || version.error)
|
|
1451
|
+
throw new Error(`Pi/OMP is still unavailable after install: ${(version.stderr || version.stdout || version.error?.message || "pi --version failed").trim()}`);
|
|
1452
|
+
}
|
|
1453
|
+
let list = runSyncCommand(["pi", "list"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
1454
|
+
if (!piListContainsRigExtension(`${list.stdout}
|
|
1455
|
+
${list.stderr}`)) {
|
|
1456
|
+
const packageSource = existsSync(resolve(projectRoot, "packages", "pi-rig", "package.json")) ? resolve(projectRoot, "packages", "pi-rig") : "npm:@h-rig/pi-rig";
|
|
1457
|
+
const install = runSyncCommand(["pi", "install", packageSource], { cwd: projectRoot, timeoutMs: 120000, spawn: deps.spawn });
|
|
1458
|
+
if (install.status !== 0 || install.error)
|
|
1459
|
+
throw new Error(`Could not install/register the Rig OMP extension: ${(install.stderr || install.stdout || install.error?.message || "pi install failed").trim()}`);
|
|
1460
|
+
list = runSyncCommand(["pi", "list"], { cwd: projectRoot, timeoutMs: 1e4, spawn: deps.spawn });
|
|
1461
|
+
if (!piListContainsRigExtension(`${list.stdout}
|
|
1462
|
+
${list.stderr}`))
|
|
1463
|
+
throw new Error("Pi/OMP is installed, but `pi list` does not show the Rig extension. Run `pi install npm:@h-rig/pi-rig` and retry Setup.");
|
|
1464
|
+
}
|
|
1465
|
+
return { ok: true, detail: (version.stdout || version.stderr).trim() || "pi available; rig extension registered" };
|
|
1466
|
+
}
|
|
1467
|
+
async function applyRigSetupProject(input) {
|
|
1468
|
+
const repo = parseRepoSlug(input.slug);
|
|
1469
|
+
ensureRigPrivateDirs(input.projectRoot);
|
|
1470
|
+
ensureGitignoreEntries(input.projectRoot);
|
|
1471
|
+
writeRigConnectionState(input.projectRoot, repo.slug, input.placement);
|
|
1472
|
+
if (input.rewriteConfig)
|
|
1473
|
+
writeRigConfig(input.projectRoot, repo.slug);
|
|
1474
|
+
const labels = await ensureGitHubLabels({ projectRoot: input.projectRoot, slug: repo.slug, deps: input.deps });
|
|
1475
|
+
const pi = input.ensurePi === false ? { skipped: true } : ensurePiRigInstalledForSetup(input.projectRoot, input.deps);
|
|
1476
|
+
return { repoSlug: repo.slug, placement: input.placement.alias, configWritten: input.rewriteConfig, labels, pi };
|
|
1477
|
+
}
|
|
1478
|
+
function hasInteractiveSetupTerminal(ctx) {
|
|
1479
|
+
return Boolean(ctx.hasUI && process.stdin.isTTY && process.stdout.isTTY);
|
|
1480
|
+
}
|
|
1481
|
+
async function promptSetupPlacement(ctx, bridge) {
|
|
1482
|
+
const targets = await listServerTargets(ctx, bridge);
|
|
1483
|
+
const options = [
|
|
1484
|
+
...targets.map((target2) => `${target2.alias} (${target2.kind})`),
|
|
1485
|
+
"Add saved remote alias"
|
|
1486
|
+
];
|
|
1487
|
+
const selected = await ctx.ui.select("Rig execution placement", options);
|
|
1488
|
+
if (!selected)
|
|
1489
|
+
throw new Error("Setup cancelled during placement selection.");
|
|
1490
|
+
if (selected === "Add saved remote alias") {
|
|
1491
|
+
const target2 = await addRemoteTarget(ctx, bridge);
|
|
1492
|
+
if (!target2)
|
|
1493
|
+
throw new Error("Remote placement was not saved.");
|
|
1494
|
+
return { alias: target2.alias, kind: target2.kind };
|
|
1495
|
+
}
|
|
1496
|
+
const alias = selected.replace(/\s+\((local|remote)\)$/, "");
|
|
1497
|
+
const target = targets.find((entry) => entry.alias === alias);
|
|
1498
|
+
if (!target)
|
|
1499
|
+
throw new Error(`Unknown placement selected: ${selected}`);
|
|
1500
|
+
return { alias: target.alias, kind: target.kind };
|
|
1501
|
+
}
|
|
1502
|
+
async function runRigSetupFlow(api, ctx, injectedBridge, deps = {}) {
|
|
1503
|
+
if (!hasInteractiveSetupTerminal(ctx))
|
|
1504
|
+
throw new Error("Rig Setup requires an interactive OMP cockpit terminal.");
|
|
1505
|
+
const projectRoot = projectRootFromContext(ctx);
|
|
1506
|
+
const existingConfig = readRigConfigStatus(projectRoot);
|
|
1507
|
+
let configAction = existingConfig.exists ? "repair" : "reconfigure";
|
|
1508
|
+
if (existingConfig.exists) {
|
|
1509
|
+
const choice = await ctx.ui.select("rig.config.ts already exists", [
|
|
1510
|
+
"Repair generated config",
|
|
1511
|
+
"Reconfigure and rewrite config",
|
|
1512
|
+
"Leave config unchanged; update private state only",
|
|
1513
|
+
"Cancel"
|
|
1514
|
+
]);
|
|
1515
|
+
if (!choice || choice === "Cancel")
|
|
1516
|
+
throw new Error("Setup cancelled.");
|
|
1517
|
+
if (choice === "Reconfigure and rewrite config")
|
|
1518
|
+
configAction = "reconfigure";
|
|
1519
|
+
else if (choice === "Leave config unchanged; update private state only")
|
|
1520
|
+
configAction = "private-state-only";
|
|
1521
|
+
else
|
|
1522
|
+
configAction = "repair";
|
|
1523
|
+
}
|
|
1524
|
+
const detectedSlug = detectOriginRepoSlug(projectRoot, deps) ?? existingConfig.slug ?? readRigConnectionStatus(projectRoot).project ?? "";
|
|
1525
|
+
const slugInput = (await ctx.ui.editor("GitHub repo slug", detectedSlug))?.trim();
|
|
1526
|
+
if (!slugInput)
|
|
1527
|
+
throw new Error("GitHub repo slug is required.");
|
|
1528
|
+
const repo = parseRepoSlug(slugInput);
|
|
1529
|
+
const placement = await promptSetupPlacement(ctx, selectedServerBridge(ctx, injectedBridge));
|
|
1530
|
+
const auth = await ensureGitHubAuthForSetup(projectRoot, repo.slug, ctx, deps);
|
|
1531
|
+
if (!auth.ok)
|
|
1532
|
+
throw new Error(auth.detail);
|
|
1533
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
1534
|
+
const probe = await probeGitHubRepository({
|
|
1535
|
+
owner: repo.owner,
|
|
1536
|
+
repo: repo.repo,
|
|
1537
|
+
token,
|
|
1538
|
+
scopes: auth.status?.scopes ?? [],
|
|
1539
|
+
...deps.fetch ? { fetchRepository: deps.fetch } : {}
|
|
1540
|
+
});
|
|
1541
|
+
if (!probe.ok)
|
|
1542
|
+
throw new Error(probe.message);
|
|
1543
|
+
const rewriteConfig = configAction === "reconfigure" || configAction === "repair" && (!existingConfig.valid || existingConfig.slug !== repo.slug);
|
|
1544
|
+
const result = await applyRigSetupProject({
|
|
1545
|
+
projectRoot,
|
|
1546
|
+
slug: repo.slug,
|
|
1547
|
+
placement,
|
|
1548
|
+
rewriteConfig,
|
|
1549
|
+
deps
|
|
1550
|
+
});
|
|
1551
|
+
const status = await detectRigStartupStatus(ctx, deps);
|
|
1552
|
+
if (!status.configured)
|
|
1553
|
+
throw new Error(`Setup wrote state but doctor still reports: ${status.reasons.join("; ")}`);
|
|
1554
|
+
appendOperatorNote(api, `Rig Setup completed for ${repo.slug} (${placement.alias})`);
|
|
1555
|
+
const labelsPayload = result.labels;
|
|
1556
|
+
const labelsReady = Boolean(labelsPayload && typeof labelsPayload === "object" && "labels" in labelsPayload && Array.isArray(labelsPayload.labels));
|
|
1557
|
+
ctx.ui.notify(`Rig Setup complete for ${repo.slug}: ${result.placement}, labels ${labelsReady ? "ready" : "checked"}, Pi ready.`, "info");
|
|
1558
|
+
}
|
|
1559
|
+
function stringList(value) {
|
|
1560
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
|
|
1561
|
+
}
|
|
1562
|
+
var TASK_STATUSES = new Set(["draft", "open", "ready", "queued", "running", "in_progress", "under_review", "blocked", "unknown", "completed", "failed", "cancelled", "closed"]);
|
|
1563
|
+
function normalizeTaskStatus(value) {
|
|
1564
|
+
const normalized = value?.trim().replace(/\s+/g, "_").replace(/-/g, "_");
|
|
1565
|
+
return normalized && TASK_STATUSES.has(normalized) ? normalized : "unknown";
|
|
1566
|
+
}
|
|
1567
|
+
function normalizeTaskPriority(value) {
|
|
1568
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1569
|
+
return value;
|
|
1570
|
+
if (typeof value === "string" && value.trim()) {
|
|
1571
|
+
const parsed = Number(value);
|
|
1572
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1573
|
+
}
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
function taskBadgeProjection(task) {
|
|
1577
|
+
const metadata = isRecord(task.metadata) ? task.metadata : {};
|
|
1578
|
+
return {
|
|
1579
|
+
id: task.id,
|
|
1580
|
+
title: taskTitle(task),
|
|
1581
|
+
status: normalizeTaskStatus(task.status),
|
|
1582
|
+
priority: normalizeTaskPriority(task.priority ?? (typeof metadata.priority === "number" || typeof metadata.priority === "string" ? metadata.priority : null)),
|
|
1583
|
+
metadata,
|
|
1584
|
+
source: task.source,
|
|
1585
|
+
url: task.url,
|
|
1586
|
+
body: task.body,
|
|
1587
|
+
assignedTo: task.assignedTo,
|
|
1588
|
+
assignees: task.assignees,
|
|
1589
|
+
dependencies: [...stringList(task.dependencies), ...stringList(metadata.dependencies)],
|
|
1590
|
+
parentChildDeps: [...stringList(task.parentChildDeps), ...stringList(metadata.parentChildDeps)]
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
function taskAssigneeText(task) {
|
|
1594
|
+
const assignees = [
|
|
1595
|
+
...stringList(task.assignees),
|
|
1596
|
+
...typeof task.assignedTo === "string" ? [task.assignedTo] : stringList(task.assignedTo),
|
|
1597
|
+
...stringList(task.metadata?.assignees),
|
|
1598
|
+
...stringList(task.metadata?.assignedTo)
|
|
1599
|
+
];
|
|
1600
|
+
return assignees.length > 0 ? `assigned ${assignees.join(",")}` : "unassigned";
|
|
1601
|
+
}
|
|
1602
|
+
function isRecord(value) {
|
|
1603
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1604
|
+
}
|
|
1605
|
+
function readBridgeFromContainer(value) {
|
|
1606
|
+
if (!isRecord(value))
|
|
1607
|
+
return null;
|
|
1608
|
+
const selectedServer = value.selectedServer;
|
|
1609
|
+
return isRecord(selectedServer) ? selectedServer : null;
|
|
1610
|
+
}
|
|
1611
|
+
function selectedServerBridge(ctx, injectedBridge) {
|
|
1612
|
+
if (injectedBridge)
|
|
1613
|
+
return injectedBridge;
|
|
1614
|
+
const fromContext = readBridgeFromContainer(ctx.rig);
|
|
1615
|
+
if (fromContext)
|
|
1616
|
+
return fromContext;
|
|
1617
|
+
const globalBridge = globalThis[RIG_EXTENSION_BRIDGE_GLOBAL];
|
|
1618
|
+
return readBridgeFromContainer(globalBridge);
|
|
1619
|
+
}
|
|
1620
|
+
function taskId(task) {
|
|
1621
|
+
const id = task?.id.trim();
|
|
1622
|
+
return id && id.length > 0 ? id : null;
|
|
1623
|
+
}
|
|
1624
|
+
function taskTitle(task) {
|
|
1625
|
+
const title = task?.title?.trim();
|
|
1626
|
+
return title && title.length > 0 ? title : taskId(task) ?? "Untitled task";
|
|
1627
|
+
}
|
|
1628
|
+
function taskStatus(task) {
|
|
1629
|
+
const status = task?.status?.trim();
|
|
1630
|
+
return status && status.length > 0 ? status : "unknown";
|
|
1631
|
+
}
|
|
1632
|
+
function taskDescription(task) {
|
|
1633
|
+
return [task.source, task.url, task.body].flatMap((part) => {
|
|
1634
|
+
const text = part?.trim();
|
|
1635
|
+
return text ? [text] : [];
|
|
1636
|
+
}).join(" \xB7 ");
|
|
1637
|
+
}
|
|
1638
|
+
function decodeActionValue(value) {
|
|
1639
|
+
try {
|
|
1640
|
+
return decodeURIComponent(value);
|
|
1641
|
+
} catch {
|
|
1642
|
+
return value;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function selectedTaskIdFromDispatchAction(id) {
|
|
1646
|
+
const prefix = "task-detail:dispatch:";
|
|
1647
|
+
if (!id.startsWith(prefix))
|
|
1648
|
+
return null;
|
|
1649
|
+
const decoded = decodeActionValue(id.slice(prefix.length)).trim();
|
|
1650
|
+
return decoded.length > 0 ? decoded : null;
|
|
1651
|
+
}
|
|
1652
|
+
function appendTaskSelected(api, task) {
|
|
1653
|
+
const id = taskId(task);
|
|
1654
|
+
if (!id)
|
|
1655
|
+
return;
|
|
1656
|
+
api.appendEntry(RIG_WORKFLOW_TASK_SELECTED, createWorkflowTaskSelected({
|
|
1657
|
+
taskId: id,
|
|
1658
|
+
title: taskTitle(task)
|
|
1659
|
+
}));
|
|
1660
|
+
}
|
|
1661
|
+
function notificationMessage(error) {
|
|
1662
|
+
return error instanceof Error ? error.message : String(error);
|
|
1663
|
+
}
|
|
1664
|
+
async function dispatchSelectedTask(api, ctx, id, injectedBridge) {
|
|
1665
|
+
const taskIdValue = selectedTaskIdFromDispatchAction(id);
|
|
1666
|
+
if (!taskIdValue)
|
|
1667
|
+
throw new Error("Task detail dispatch requires a selected task id.");
|
|
1668
|
+
const bridge = selectedServerBridge(ctx, injectedBridge);
|
|
1669
|
+
if (!bridge?.getTask || !bridge.dispatchTask) {
|
|
1670
|
+
throw new Error("Rig selected-server task dispatch bridge is unavailable.");
|
|
1671
|
+
}
|
|
1672
|
+
const task = await bridge.getTask(taskIdValue);
|
|
1673
|
+
if (!task)
|
|
1674
|
+
throw new Error(`Selected task not found: ${taskIdValue}`);
|
|
1675
|
+
appendTaskSelected(api, task);
|
|
1676
|
+
appendStatus(api, "starting", `Dispatching selected task ${taskIdValue} through the selected server target.`);
|
|
1677
|
+
const result = await bridge.dispatchTask({ taskId: taskIdValue, ...task.title ? { title: task.title } : {} });
|
|
1678
|
+
appendStatus(api, "running", `Selected task ${taskIdValue} dispatched as run ${result.runId}.`);
|
|
1679
|
+
if (bridge.attachRun) {
|
|
1680
|
+
const attach = await bridge.attachRun({ runId: result.runId, ...taskIdValue ? { taskId: taskIdValue } : {} });
|
|
1681
|
+
const joinLink = attach?.joinLink ?? result.joinLink ?? null;
|
|
1682
|
+
if (joinLink) {
|
|
1683
|
+
await joinCollab(ctx, { joinLink });
|
|
1684
|
+
ctx.ui.notify(`Attached ${result.runId.slice(0, 8)} through OMP collab.`, "info");
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const sessionPath = attach?.sessionPath ?? result.sessionPath ?? null;
|
|
1688
|
+
const switchSession = ctx.switchSession;
|
|
1689
|
+
if (sessionPath && switchSession) {
|
|
1690
|
+
const switched = await switchSession(sessionPath);
|
|
1691
|
+
if (!switched.cancelled) {
|
|
1692
|
+
ctx.ui.notify(`Attached ${result.runId.slice(0, 8)} through OMP collab.`, "info");
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
ctx.ui.notify(`Dispatched ${taskIdValue} as ${result.runId.slice(0, 8)} \u2014 open Runs to attach when it comes online.`, "info");
|
|
1698
|
+
}
|
|
1699
|
+
function normalizeSpawnTargetState(server) {
|
|
1700
|
+
if (!server)
|
|
1701
|
+
return null;
|
|
1702
|
+
return { ...server, status: "ready" };
|
|
1703
|
+
}
|
|
1704
|
+
async function collectDoctorChecks(api, ctx) {
|
|
1705
|
+
const checks = [];
|
|
1706
|
+
const identity = resolveRigIdentity(ctx);
|
|
1707
|
+
checks.push({ label: "identity", level: identity ? "ok" : "warn", detail: identity?.source ?? "GitHub identity unavailable" });
|
|
1708
|
+
const filter = resolveRigIdentityFilter(ctx);
|
|
1709
|
+
checks.push({ label: "github-repo", level: filter?.selectedRepo ? "ok" : "warn", detail: filter?.selectedRepo ?? "repo not selected" });
|
|
1710
|
+
const rigCtx = ctx;
|
|
1711
|
+
checks.push({ label: "collab-host", level: rigCtx.collab?.startHost || rigCtx.collab?.startCollabHost ? "ok" : "fail", detail: "OMP host facade" });
|
|
1712
|
+
checks.push({ label: "collab-join", level: rigCtx.collab?.join || rigCtx.collab?.joinCollabSession ? "ok" : "fail", detail: "OMP join facade" });
|
|
1713
|
+
checks.push({ label: "collab-registry", level: rigCtx.collab?.listActive || rigCtx.collab?.listActiveCollabSessions || rigCtx.collab?.listSessions ? "ok" : "warn", detail: "live registry facade" });
|
|
1714
|
+
try {
|
|
1715
|
+
const sessions = identity ? await discoverRigSessions(ctx) : [];
|
|
1716
|
+
checks.push({ label: "run-discovery", level: "ok", detail: `${sessions.length} run session(s)` });
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
checks.push({ label: "run-discovery", level: "fail", detail: notificationMessage(error) });
|
|
1719
|
+
}
|
|
1720
|
+
try {
|
|
1721
|
+
const targets = await listServerTargets(ctx, selectedServerBridge(ctx));
|
|
1722
|
+
checks.push({ label: "relay-targets", level: targets.length > 0 ? "ok" : "warn", detail: `${targets.length} target(s)` });
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
checks.push({ label: "relay-targets", level: "warn", detail: notificationMessage(error) });
|
|
1725
|
+
}
|
|
1726
|
+
const note = `Doctor session seam probe ${new Date().toISOString()}`;
|
|
1727
|
+
appendOperatorNote(api, note);
|
|
1728
|
+
checks.push({ label: "custom-entry", level: currentWorkflow(ctx).notes.some((entry) => entry.note === note) ? "ok" : "fail", detail: RIG_WORKFLOW_OPERATOR_NOTE });
|
|
1729
|
+
checks.push({ label: "pi-extension", level: "ok", detail: "Rig extension registered" });
|
|
1730
|
+
return checks;
|
|
1731
|
+
}
|
|
1732
|
+
function doctorCheckRows(checks) {
|
|
1733
|
+
if (!checks) {
|
|
1734
|
+
return [{ id: "doctor:loading", label: "CHECKS", currentValue: "loading", heading: true, description: "Doctor rows render persistent pass/warn/fail checks." }];
|
|
1735
|
+
}
|
|
1736
|
+
return checks.map((check) => ({
|
|
1737
|
+
id: `doctor:check:${check.label}`,
|
|
1738
|
+
label: check.label.toUpperCase(),
|
|
1739
|
+
currentValue: check.level,
|
|
1740
|
+
heading: true,
|
|
1741
|
+
description: check.detail
|
|
1742
|
+
}));
|
|
1743
|
+
}
|
|
1744
|
+
async function loadRigFlowState(ctx, screen, selectedTaskId, injectedBridge, api) {
|
|
1745
|
+
const bridge = selectedServerBridge(ctx, injectedBridge);
|
|
1746
|
+
try {
|
|
1747
|
+
const needsServer = screen === "cockpit" || screen === "runs" || screen === "server" || screen === "tasks" || screen === "task-detail" || screen === "setup";
|
|
1748
|
+
const server = needsServer ? normalizeSpawnTargetState(await bridge?.readServerState?.() ?? null) : null;
|
|
1749
|
+
const serverTargets = screen === "server" || screen === "setup" ? await listServerTargets(ctx, bridge) : undefined;
|
|
1750
|
+
const tasks = screen === "tasks" && bridge?.listTasks ? await bridge.listTasks() : undefined;
|
|
1751
|
+
const currentUserLogin = resolveRigIdentity(ctx)?.owner.login ?? null;
|
|
1752
|
+
const selectedTask = screen === "task-detail" && selectedTaskId && bridge?.getTask ? await bridge.getTask(selectedTaskId) : null;
|
|
1753
|
+
const doctorChecks = screen === "doctor" && api ? await collectDoctorChecks(api, ctx) : undefined;
|
|
1754
|
+
const setupStatus = screen === "setup" ? await detectRigStartupStatus(ctx) : undefined;
|
|
1755
|
+
return {
|
|
1756
|
+
...server ? { server } : {},
|
|
1757
|
+
...serverTargets ? { serverTargets } : {},
|
|
1758
|
+
...tasks ? { tasks } : {},
|
|
1759
|
+
...currentUserLogin ? { currentUserLogin } : {},
|
|
1760
|
+
...selectedTask ? { selectedTask, selectedTaskId } : selectedTaskId ? { selectedTaskId } : {},
|
|
1761
|
+
...doctorChecks ? { doctorChecks } : {},
|
|
1762
|
+
...setupStatus ? { setupStatus } : {}
|
|
1763
|
+
};
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
return { error: notificationMessage(error) };
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
function identityMatchesWorkflow(identity, workflow) {
|
|
1769
|
+
const started = workflow.started;
|
|
1770
|
+
if (!started)
|
|
1771
|
+
return false;
|
|
1772
|
+
if (started.selectedRepo !== identity.selectedRepo)
|
|
1773
|
+
return false;
|
|
1774
|
+
if (started.owner.githubUserId !== identity.owner.githubUserId)
|
|
1775
|
+
return false;
|
|
1776
|
+
if (started.owner.namespaceKey !== identity.owner.namespaceKey)
|
|
1777
|
+
return false;
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
function collabMatchesIdentity(identity, collab) {
|
|
1781
|
+
if (collab.selectedRepo && collab.selectedRepo !== identity.selectedRepo)
|
|
1782
|
+
return false;
|
|
1783
|
+
if (collab.owner?.githubUserId && collab.owner.githubUserId !== identity.owner.githubUserId)
|
|
1784
|
+
return false;
|
|
1785
|
+
if (collab.owner?.namespaceKey && collab.owner.namespaceKey !== identity.owner.namespaceKey)
|
|
1786
|
+
return false;
|
|
1787
|
+
return true;
|
|
1788
|
+
}
|
|
1789
|
+
function isJoinableCollab(collab) {
|
|
1790
|
+
return Boolean(collab?.joinLink && !collab.stale);
|
|
1791
|
+
}
|
|
1792
|
+
function hasRigWorkflow(workflow, collab, run) {
|
|
1793
|
+
return Boolean(collab || run?.status || Object.keys(run?.record ?? {}).length > 0 || workflow.started || workflow.target || workflow.task || workflow.status || workflow.notes.length > 0 || workflow.inbox.length > 0 || workflow.resolvedInbox.length > 0);
|
|
1794
|
+
}
|
|
1795
|
+
function sessionBelongsToWorkspace(session, workflow, collab, ctx) {
|
|
1796
|
+
const cwd = resolve(ctx.sessionManager.getCwd());
|
|
1797
|
+
const sessionCwd = session.cwd ? resolve(session.cwd) : "";
|
|
1798
|
+
const collabCwd = collab?.cwd ? resolve(collab.cwd) : "";
|
|
1799
|
+
const identity = resolveRigIdentity(ctx);
|
|
1800
|
+
if (!identity) {
|
|
1801
|
+
if (!collab)
|
|
1802
|
+
return false;
|
|
1803
|
+
const fallbackNamespace = resolveOwnerNamespaceKey(rigProjectRoot());
|
|
1804
|
+
if (!fallbackNamespace || collab.owner?.namespaceKey !== fallbackNamespace)
|
|
1805
|
+
return false;
|
|
1806
|
+
return sessionCwd === cwd || collabCwd === cwd || !collab.cwd;
|
|
1807
|
+
}
|
|
1808
|
+
if (collab && !collabMatchesIdentity(identity, collab))
|
|
1809
|
+
return false;
|
|
1810
|
+
if (sessionCwd !== cwd && collabCwd !== cwd)
|
|
1811
|
+
return Boolean(collab && !collab.cwd);
|
|
1812
|
+
return collab ? true : identityMatchesWorkflow(identity, workflow);
|
|
1813
|
+
}
|
|
1814
|
+
function customEntries2(entries) {
|
|
1815
|
+
const customs = [];
|
|
1816
|
+
for (const entry of entries) {
|
|
1817
|
+
if (entry.type === "custom")
|
|
1818
|
+
customs.push(entry);
|
|
1819
|
+
}
|
|
1820
|
+
return customs;
|
|
1821
|
+
}
|
|
1822
|
+
function currentWorkflow(ctx) {
|
|
1823
|
+
return projectWorkflowEntries(customEntries2(ctx.sessionManager.getBranch()));
|
|
1824
|
+
}
|
|
1825
|
+
function runIdForProjection(item) {
|
|
1826
|
+
const title = item.collab?.title || item.session.title || item.session.firstMessage || item.session.id;
|
|
1827
|
+
if (title.startsWith("Rig run "))
|
|
1828
|
+
return title.slice("Rig run ".length).trim();
|
|
1829
|
+
if (title.startsWith("rig-run-"))
|
|
1830
|
+
return title.slice("rig-run-".length).trim();
|
|
1831
|
+
return item.session.id;
|
|
1832
|
+
}
|
|
1833
|
+
function timelineEntriesFromCustomEntries(entries) {
|
|
1834
|
+
return entries.flatMap((entry) => {
|
|
1835
|
+
if (entry.customType !== CUSTOM_TYPE_FOR["timeline-entry"] || !isRecord(entry.data))
|
|
1836
|
+
return [];
|
|
1837
|
+
const payload = isRecord(entry.data.payload) ? entry.data.payload : entry.data;
|
|
1838
|
+
const type = typeof payload.type === "string" ? payload.type : "timeline";
|
|
1839
|
+
const stage = typeof payload.stage === "string" ? payload.stage : typeof payload.name === "string" ? payload.name : null;
|
|
1840
|
+
const status = typeof payload.status === "string" ? payload.status : typeof payload.outcome === "string" ? payload.outcome : null;
|
|
1841
|
+
const detail = typeof payload.detail === "string" ? payload.detail : typeof payload.message === "string" ? payload.message : null;
|
|
1842
|
+
const at = typeof entry.data.at === "string" ? entry.data.at : typeof payload.at === "string" ? payload.at : null;
|
|
1843
|
+
return [{ at, type, stage, status, detail }];
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
function rigProjectRoot() {
|
|
1847
|
+
return process.env.PROJECT_RIG_ROOT?.trim() || process.env.RIG_HOST_PROJECT_ROOT?.trim() || process.cwd();
|
|
1848
|
+
}
|
|
1849
|
+
function registrySecret() {
|
|
1850
|
+
return resolveRegistrySecret(rigProjectRoot()) ?? null;
|
|
1851
|
+
}
|
|
1852
|
+
var RUN_PROCESS_STEER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
1853
|
+
var TRACKED_RUN_STALL_MS = 20 * 60 * 1000;
|
|
1854
|
+
var RUN_PROCESS_STALL_SWEEP_MS = 60 * 1000;
|
|
1855
|
+
var RUN_PROCESS_STALL_DETAIL = "Run process made no OMP session progress for 20+ minutes; recording a stall so recovery can requeue or resume the session.";
|
|
1856
|
+
function timestampMs(value) {
|
|
1857
|
+
if (value === null || value === undefined)
|
|
1858
|
+
return null;
|
|
1859
|
+
const ms = value instanceof Date ? value.getTime() : typeof value === "number" ? value : Date.parse(value);
|
|
1860
|
+
return Number.isFinite(ms) ? ms : null;
|
|
1861
|
+
}
|
|
1862
|
+
function computeRunStall(input) {
|
|
1863
|
+
const lastActivityAt = timestampMs(input.lastActivityAt);
|
|
1864
|
+
const now = timestampMs(input.now);
|
|
1865
|
+
if (lastActivityAt === null || now === null || input.thresholdMs <= 0)
|
|
1866
|
+
return false;
|
|
1867
|
+
return now - lastActivityAt >= input.thresholdMs;
|
|
1868
|
+
}
|
|
1869
|
+
function registryBaseUrl() {
|
|
1870
|
+
return resolveRegistryBaseUrl(rigProjectRoot());
|
|
1871
|
+
}
|
|
1872
|
+
function rigRelayUrl() {
|
|
1873
|
+
return resolveRelayUrl();
|
|
1874
|
+
}
|
|
1875
|
+
function registryClientFor(ctx, filter) {
|
|
1876
|
+
const identity = resolveRigIdentity(ctx);
|
|
1877
|
+
const namespaceKey = filter.namespaceKey ?? identity?.owner.namespaceKey ?? resolveOwnerNamespaceKey(rigProjectRoot());
|
|
1878
|
+
const secret = registrySecret();
|
|
1879
|
+
if (!namespaceKey || !secret)
|
|
1880
|
+
return null;
|
|
1881
|
+
return createRegistryClient({ baseUrl: registryBaseUrl(), namespaceKey, secret });
|
|
1882
|
+
}
|
|
1883
|
+
function collabFromRegistryEntry(entry) {
|
|
1884
|
+
return {
|
|
1885
|
+
sessionId: entry.roomId,
|
|
1886
|
+
sessionPath: "",
|
|
1887
|
+
cwd: "",
|
|
1888
|
+
title: entry.title,
|
|
1889
|
+
joinLink: entry.joinLink,
|
|
1890
|
+
webLink: entry.webLink,
|
|
1891
|
+
relayUrl: entry.relayUrl,
|
|
1892
|
+
owner: entry.owner,
|
|
1893
|
+
selectedRepo: entry.repo,
|
|
1894
|
+
startedAt: entry.startedAt,
|
|
1895
|
+
updatedAt: entry.heartbeatAt,
|
|
1896
|
+
...entry.pid === undefined ? {} : { pid: entry.pid },
|
|
1897
|
+
stale: entry.stale
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function sessionProjectionFromCollab(collab) {
|
|
1901
|
+
const started = Date.parse(collab.startedAt);
|
|
1902
|
+
const updated = Date.parse(collab.updatedAt);
|
|
1903
|
+
const modified = new Date(Number.isFinite(updated) ? updated : Date.now());
|
|
1904
|
+
return {
|
|
1905
|
+
session: {
|
|
1906
|
+
path: collab.sessionPath || collab.sessionId,
|
|
1907
|
+
id: collab.sessionId,
|
|
1908
|
+
cwd: collab.cwd,
|
|
1909
|
+
title: collab.title,
|
|
1910
|
+
created: new Date(Number.isFinite(started) ? started : modified.getTime()),
|
|
1911
|
+
modified,
|
|
1912
|
+
messageCount: 0,
|
|
1913
|
+
size: 0,
|
|
1914
|
+
firstMessage: collab.title || "Live OMP collab session",
|
|
1915
|
+
allMessagesText: collab.title || "Live OMP collab session",
|
|
1916
|
+
status: collab.stale ? "unknown" : "pending"
|
|
1917
|
+
},
|
|
1918
|
+
collab,
|
|
1919
|
+
customEntries: [],
|
|
1920
|
+
source: "collab-registry"
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
async function listActiveCollab(ctx, filter) {
|
|
1924
|
+
const facade = ctx.collab;
|
|
1925
|
+
if (facade?.listActive)
|
|
1926
|
+
return facade.listActive(filter);
|
|
1927
|
+
if (facade?.listActiveCollabSessions)
|
|
1928
|
+
return facade.listActiveCollabSessions(filter);
|
|
1929
|
+
const client = registryClientFor(ctx, filter);
|
|
1930
|
+
if (!client)
|
|
1931
|
+
return [];
|
|
1932
|
+
return (await client.listRoomsByOwner(filter)).map(collabFromRegistryEntry);
|
|
1933
|
+
}
|
|
1934
|
+
async function listCollabSessionViews(ctx, filter) {
|
|
1935
|
+
const facade = ctx.collab;
|
|
1936
|
+
if (facade?.listSessions)
|
|
1937
|
+
return facade.listSessions(filter);
|
|
1938
|
+
return (await listActiveCollab(ctx, filter)).map(sessionProjectionFromCollab);
|
|
1939
|
+
}
|
|
1940
|
+
async function joinCollab(ctx, input) {
|
|
1941
|
+
const facade = ctx.collab;
|
|
1942
|
+
if (facade?.join) {
|
|
1943
|
+
await facade.join(input);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (facade?.joinCollabSession) {
|
|
1947
|
+
await facade.joinCollabSession(input);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
throw new Error("OMP collab facade is unavailable for this extension context.");
|
|
1951
|
+
}
|
|
1952
|
+
function appendStatus(api, status, detail) {
|
|
1953
|
+
api.appendEntry(RIG_WORKFLOW_STATUS_CHANGED, createWorkflowStatusChanged({
|
|
1954
|
+
status,
|
|
1955
|
+
...detail ? { detail } : {}
|
|
1956
|
+
}));
|
|
1957
|
+
}
|
|
1958
|
+
function appendWorkflowStatus(sessionManager, status, detail) {
|
|
1959
|
+
sessionManager.appendCustomEntry(RIG_WORKFLOW_STATUS_CHANGED, createWorkflowStatusChanged({
|
|
1960
|
+
status,
|
|
1961
|
+
...detail ? { detail } : {}
|
|
1962
|
+
}));
|
|
1963
|
+
}
|
|
1964
|
+
function hasRecordedWorkflow(workflow) {
|
|
1965
|
+
return Boolean(workflow.started || workflow.target || workflow.task || workflow.status || workflow.notes.length > 0 || workflow.inbox.length > 0 || workflow.resolvedInbox.length > 0);
|
|
1966
|
+
}
|
|
1967
|
+
function workflowWithCollabMarker(workflow, collab) {
|
|
1968
|
+
if (!collab || hasRecordedWorkflow(workflow))
|
|
1969
|
+
return workflow;
|
|
1970
|
+
return projectCollabWorkflowMarker(collab);
|
|
1971
|
+
}
|
|
1972
|
+
function collabHostStartedFromEntries(entries) {
|
|
1973
|
+
let found = null;
|
|
1974
|
+
for (const entry of entries) {
|
|
1975
|
+
if (entry.customType !== CUSTOM_TYPE_FOR["timeline-entry"] || !isRecord(entry.data))
|
|
1976
|
+
continue;
|
|
1977
|
+
const payload = isRecord(entry.data.payload) ? entry.data.payload : entry.data;
|
|
1978
|
+
if (payload.type !== "collab-host-started")
|
|
1979
|
+
continue;
|
|
1980
|
+
found = {
|
|
1981
|
+
roomId: typeof payload.roomId === "string" ? payload.roomId : undefined,
|
|
1982
|
+
joinLink: typeof payload.joinLink === "string" ? payload.joinLink : undefined,
|
|
1983
|
+
webLink: typeof payload.webLink === "string" ? payload.webLink : undefined,
|
|
1984
|
+
relayUrl: typeof payload.relayUrl === "string" ? payload.relayUrl : undefined,
|
|
1985
|
+
at: typeof entry.data.at === "string" ? entry.data.at : typeof payload.at === "string" ? payload.at : undefined
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
return found;
|
|
1989
|
+
}
|
|
1990
|
+
async function discoverLocalRunSessions(ctx) {
|
|
1991
|
+
const projectRoot = rigProjectRoot();
|
|
1992
|
+
let runtimes;
|
|
1993
|
+
try {
|
|
1994
|
+
runtimes = await listAgentRuntimes(projectRoot);
|
|
1995
|
+
} catch {
|
|
1996
|
+
return [];
|
|
1997
|
+
}
|
|
1998
|
+
const identity = resolveRigIdentity(ctx);
|
|
1999
|
+
const owner = identity?.owner ?? { githubUserId: "", login: "", namespaceKey: resolveOwnerNamespaceKey(projectRoot) ?? "" };
|
|
2000
|
+
const out = [];
|
|
2001
|
+
for (const runtime of runtimes) {
|
|
2002
|
+
const sessionDir = runtime.sessionDir;
|
|
2003
|
+
if (!sessionDir || !existsSync(sessionDir))
|
|
2004
|
+
continue;
|
|
2005
|
+
let sessionFiles;
|
|
2006
|
+
try {
|
|
2007
|
+
sessionFiles = readdirSync(sessionDir).filter((f) => f.endsWith(".jsonl")).sort().reverse().map((f) => resolve(sessionDir, f));
|
|
2008
|
+
} catch {
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
for (const sessionFile of sessionFiles) {
|
|
2012
|
+
try {
|
|
2013
|
+
const manager = await SessionManager.open(sessionFile, sessionDir, undefined, { suppressBreadcrumb: true });
|
|
2014
|
+
const customs = customEntries2(manager.getBranch());
|
|
2015
|
+
const host = collabHostStartedFromEntries(customs);
|
|
2016
|
+
if (!host?.joinLink)
|
|
2017
|
+
continue;
|
|
2018
|
+
const at = host.at ?? new Date().toISOString();
|
|
2019
|
+
const collab = {
|
|
2020
|
+
sessionId: host.roomId || sessionFile,
|
|
2021
|
+
sessionPath: sessionFile,
|
|
2022
|
+
cwd: manager.getCwd(),
|
|
2023
|
+
title: manager.getSessionName() || `Rig run ${runtime.id}`,
|
|
2024
|
+
joinLink: host.joinLink,
|
|
2025
|
+
webLink: host.webLink ?? "",
|
|
2026
|
+
relayUrl: host.relayUrl ?? "",
|
|
2027
|
+
owner,
|
|
2028
|
+
selectedRepo: identity?.selectedRepo ?? "",
|
|
2029
|
+
startedAt: at,
|
|
2030
|
+
updatedAt: at,
|
|
2031
|
+
stale: false
|
|
2032
|
+
};
|
|
2033
|
+
out.push({ ...sessionProjectionFromCollab(collab), customEntries: customs, source: "session-list" });
|
|
2034
|
+
break;
|
|
2035
|
+
} catch {
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return out;
|
|
2041
|
+
}
|
|
2042
|
+
async function discoverRigSessions(ctx) {
|
|
2043
|
+
const identityFilter = ctx ? resolveRigIdentityFilter(ctx) : undefined;
|
|
2044
|
+
const fallbackNamespace = resolveOwnerNamespaceKey(rigProjectRoot());
|
|
2045
|
+
const discoveryFilter = identityFilter ?? (fallbackNamespace ? { namespaceKey: fallbackNamespace } : {});
|
|
2046
|
+
const [viaRunScan, viaRegistry, viaLocal, viaRigRegistry] = await Promise.all([
|
|
2047
|
+
ctx ? discoverLocalRunSessions(ctx).catch(() => []) : Promise.resolve([]),
|
|
2048
|
+
ctx ? listCollabSessionViews(ctx, discoveryFilter).catch(() => []) : Promise.resolve([]),
|
|
2049
|
+
listCollabSessionProjections(discoveryFilter).catch(() => []),
|
|
2050
|
+
ctx ? (async () => {
|
|
2051
|
+
const client = registryClientFor(ctx, discoveryFilter);
|
|
2052
|
+
if (!client)
|
|
2053
|
+
return [];
|
|
2054
|
+
return (await client.listRoomsByOwner(discoveryFilter)).map(collabFromRegistryEntry).map(sessionProjectionFromCollab);
|
|
2055
|
+
})().catch(() => []) : Promise.resolve([])
|
|
2056
|
+
]);
|
|
2057
|
+
const seen = new Set;
|
|
2058
|
+
const discovered = [];
|
|
2059
|
+
for (const item of [...viaRunScan, ...viaRegistry, ...viaLocal, ...viaRigRegistry]) {
|
|
2060
|
+
const key = item.session.path || item.collab?.sessionId || item.session.id;
|
|
2061
|
+
if (!key || seen.has(key))
|
|
2062
|
+
continue;
|
|
2063
|
+
seen.add(key);
|
|
2064
|
+
discovered.push(item);
|
|
2065
|
+
}
|
|
2066
|
+
return discovered.map((item) => {
|
|
2067
|
+
const entries = customEntries2(item.customEntries);
|
|
2068
|
+
const workflow = workflowWithCollabMarker(projectWorkflowEntries(entries), item.collab);
|
|
2069
|
+
const run = foldRunSessionEntries(entries, runIdForProjection(item));
|
|
2070
|
+
const timeline = timelineEntriesFromCustomEntries(entries);
|
|
2071
|
+
return { session: item.session, workflow, collab: item.collab, run, timeline };
|
|
2072
|
+
}).filter((projection) => hasRigWorkflow(projection.workflow, projection.collab, projection.run)).filter((projection) => !ctx || sessionBelongsToWorkspace(projection.session, projection.workflow, projection.collab, ctx)).sort((a, b) => projectionUpdatedAt(b) - projectionUpdatedAt(a));
|
|
2073
|
+
}
|
|
2074
|
+
async function joinRun(ctx, encodedSessionPath) {
|
|
2075
|
+
const sessionPath = decodeURIComponent(encodedSessionPath);
|
|
2076
|
+
const session = (await discoverRigSessions(ctx)).find((item) => item.session.path === sessionPath);
|
|
2077
|
+
if (!session) {
|
|
2078
|
+
ctx.ui.notify("Run selection is stale; refresh runs before joining.", "warning");
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
const collab = session.collab;
|
|
2082
|
+
if (!isJoinableCollab(collab)) {
|
|
2083
|
+
const stale = Boolean(collab?.stale);
|
|
2084
|
+
ctx.ui.notify(stale ? "Selected OMP collab session is stale; refresh runs before joining." : "Selected OMP session is not currently available through collab.", "warning");
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
await joinCollab(ctx, { joinLink: collab.joinLink, cwd: collab.cwd || session.session.cwd });
|
|
2088
|
+
ctx.ui.notify(`Joined ${session.session.id.slice(0, 8)} through OMP collab: ${collabLinks(collab)}`, "info");
|
|
2089
|
+
}
|
|
2090
|
+
function stopSentinel(runId, reason) {
|
|
2091
|
+
return buildStopSentinel(runId, reason, "rig-extension");
|
|
2092
|
+
}
|
|
2093
|
+
async function steerRun(_api, ctx, encodedSessionPath, stop = false) {
|
|
2094
|
+
const sessionPath = decodeURIComponent(encodedSessionPath);
|
|
2095
|
+
const session = (await discoverRigSessions(ctx)).find((item) => item.session.path === sessionPath);
|
|
2096
|
+
if (!session)
|
|
2097
|
+
throw new Error("Run selection is stale; refresh runs before steering.");
|
|
2098
|
+
const runId = session.run?.record.runId ?? session.session.id;
|
|
2099
|
+
const prompt = stop ? stopSentinel(runId, await ctx.ui.editor("Stop reason (optional)", "operator requested stop")) : (await ctx.ui.editor("Steer Rig run", ""))?.trim();
|
|
2100
|
+
if (!prompt)
|
|
2101
|
+
return;
|
|
2102
|
+
let guest = ctx.collabGuest;
|
|
2103
|
+
if (!guest?.sendPrompt) {
|
|
2104
|
+
await joinRun(ctx, encodeURIComponent(session.session.path));
|
|
2105
|
+
guest = ctx.collabGuest;
|
|
2106
|
+
}
|
|
2107
|
+
if (!guest?.sendPrompt)
|
|
2108
|
+
throw new Error("Joined run did not expose an OMP collab guest prompt transport.");
|
|
2109
|
+
guest.sendPrompt(prompt);
|
|
2110
|
+
ctx.ui.notify(stop ? `Stop requested for ${runId}.` : `Steered ${runId}.`, "info");
|
|
2111
|
+
}
|
|
2112
|
+
function inboxActionId(session, request) {
|
|
2113
|
+
return `inbox:${encodeURIComponent(session.path)}:${encodeURIComponent(request.requestId)}`;
|
|
2114
|
+
}
|
|
2115
|
+
function parseInboxActionTarget(id) {
|
|
2116
|
+
if (!id.startsWith("inbox:"))
|
|
2117
|
+
return null;
|
|
2118
|
+
const [encodedSessionPath, encodedRequestId] = id.slice("inbox:".length).split(":");
|
|
2119
|
+
if (!encodedSessionPath || !encodedRequestId)
|
|
2120
|
+
return null;
|
|
2121
|
+
try {
|
|
2122
|
+
const sessionPath = decodeURIComponent(encodedSessionPath);
|
|
2123
|
+
const requestId = decodeURIComponent(encodedRequestId);
|
|
2124
|
+
return sessionPath && requestId ? { sessionPath, requestId } : null;
|
|
2125
|
+
} catch {
|
|
2126
|
+
return null;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
function inboxPrompt(request) {
|
|
2130
|
+
return request.body ? `${request.title}
|
|
2131
|
+
${request.body}` : request.title;
|
|
2132
|
+
}
|
|
2133
|
+
function inboxResolutionMessage(request, decision, answer) {
|
|
2134
|
+
if (decision === "approved")
|
|
2135
|
+
return `Rig workflow inbox request "${request.title}" was approved.`;
|
|
2136
|
+
if (decision === "rejected")
|
|
2137
|
+
return `Rig workflow inbox request "${request.title}" was rejected.`;
|
|
2138
|
+
return `Rig workflow inbox request "${request.title}" was answered:
|
|
2139
|
+
${String(answer ?? "")}`;
|
|
2140
|
+
}
|
|
2141
|
+
async function appendInboxResolved(api, ctx, session, request, decision, answer) {
|
|
2142
|
+
const data = createWorkflowInboxResolved({
|
|
2143
|
+
requestId: request.requestId,
|
|
2144
|
+
decision,
|
|
2145
|
+
...decision === "answered" ? { answer: answer ?? "" } : {}
|
|
2146
|
+
});
|
|
2147
|
+
const detail = `Resolved inbox request ${request.requestId}.`;
|
|
2148
|
+
if (session.path === ctx.sessionManager.getSessionFile()) {
|
|
2149
|
+
api.appendEntry(RIG_WORKFLOW_INBOX_RESOLVED, data);
|
|
2150
|
+
appendStatus(api, "running", detail);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const target = await SessionManager.open(session.path, undefined, undefined, { suppressBreadcrumb: true });
|
|
2154
|
+
target.appendCustomEntry(RIG_WORKFLOW_INBOX_RESOLVED, data);
|
|
2155
|
+
appendWorkflowStatus(target, "running", detail);
|
|
2156
|
+
}
|
|
2157
|
+
async function resolveInboxRequest(api, ctx, id) {
|
|
2158
|
+
const target = parseInboxActionTarget(id);
|
|
2159
|
+
if (!target) {
|
|
2160
|
+
ctx.ui.notify("Inbox action is malformed; refresh the Rig Inbox.", "warning");
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const session = (await discoverRigSessions(ctx)).find((item) => item.session.path === target.sessionPath);
|
|
2164
|
+
const request = session?.workflow.inbox.find((item) => item.requestId === target.requestId);
|
|
2165
|
+
if (!session || !request) {
|
|
2166
|
+
ctx.ui.notify("Inbox request is stale or already resolved; refresh the Rig Inbox.", "warning");
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (request.kind === "approval") {
|
|
2170
|
+
const choice = await ctx.ui.select(inboxPrompt(request), ["Approve", "Reject"]);
|
|
2171
|
+
if (!choice) {
|
|
2172
|
+
ctx.ui.notify("Inbox approval cancelled.", "info");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
const decision = choice === "Approve" ? "approved" : "rejected";
|
|
2176
|
+
await appendInboxResolved(api, ctx, session.session, request, decision);
|
|
2177
|
+
api.sendUserMessage(inboxResolutionMessage(request, decision), { deliverAs: "followUp" });
|
|
2178
|
+
ctx.ui.notify(`Inbox request ${request.requestId} ${decision}.`, "info");
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
const answer = request.options && request.options.length > 0 ? await ctx.ui.select(inboxPrompt(request), [...request.options]) : await ctx.ui.editor(`Rig workflow input: ${request.title}`, request.body ?? "");
|
|
2182
|
+
if (answer === undefined) {
|
|
2183
|
+
ctx.ui.notify("Inbox input cancelled.", "info");
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
await appendInboxResolved(api, ctx, session.session, request, "answered", answer);
|
|
2187
|
+
api.sendUserMessage(inboxResolutionMessage(request, "answered", answer), { deliverAs: "followUp" });
|
|
2188
|
+
ctx.ui.notify(`Inbox request ${request.requestId} answered.`, "info");
|
|
2189
|
+
}
|
|
2190
|
+
function defaultFlowActions(api, ctx, injectedBridge) {
|
|
2191
|
+
return {
|
|
2192
|
+
async act(id) {
|
|
2193
|
+
if (id === "setup:start") {
|
|
2194
|
+
await runRigSetupFlow(api, ctx, injectedBridge);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (id === "setup:doctor") {
|
|
2198
|
+
const status = await detectRigStartupStatus(ctx);
|
|
2199
|
+
notifyDoctor(ctx, "Setup doctor", status.checks);
|
|
2200
|
+
if (status.reasons.length > 0 && ctx.hasUI)
|
|
2201
|
+
ctx.ui.notify(`Setup needs attention: ${status.reasons.join("; ")}`, "warning");
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
if (id.startsWith("server:select:")) {
|
|
2205
|
+
const alias = decodeActionValue(id.slice("server:select:".length));
|
|
2206
|
+
await selectServerTarget(ctx, selectedServerBridge(ctx, injectedBridge), alias);
|
|
2207
|
+
if (ctx.hasUI)
|
|
2208
|
+
ctx.ui.notify(`Selected Rig target ${alias}.`, "info");
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if (id === "server:add-remote") {
|
|
2212
|
+
await addRemoteTarget(ctx, selectedServerBridge(ctx, injectedBridge));
|
|
2213
|
+
if (ctx.hasUI)
|
|
2214
|
+
ctx.ui.notify("Rig remote target saved and selected.", "info");
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
if (id === "server:refresh" || id === "tasks:refresh") {
|
|
2218
|
+
if (ctx.hasUI)
|
|
2219
|
+
ctx.ui.notify("Rig Cockpit refreshed selected server/task state.", "info");
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
if (id.startsWith("task-detail:dispatch:")) {
|
|
2223
|
+
await dispatchSelectedTask(api, ctx, id, injectedBridge);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (id.startsWith("run:")) {
|
|
2227
|
+
await joinRun(ctx, id.slice("run:".length));
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
if (id.startsWith("run-steer:")) {
|
|
2231
|
+
await steerRun(api, ctx, id.slice("run-steer:".length));
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (id.startsWith("run-stop:")) {
|
|
2235
|
+
await steerRun(api, ctx, id.slice("run-stop:".length), true);
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
if (id.startsWith("inbox:")) {
|
|
2239
|
+
await resolveInboxRequest(api, ctx, id);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
if (id === "doctor:sessions") {
|
|
2243
|
+
await runSessionDoctor(api, ctx);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
if (id === "doctor:collab") {
|
|
2247
|
+
await runCollabDoctor(ctx);
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
if (ctx.hasUI)
|
|
2251
|
+
ctx.ui.notify(`Unknown Rig action: ${id}`, "warning");
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
function notifyDoctor(ctx, title, checks) {
|
|
2256
|
+
const failed = checks.filter((check) => check.level === "fail");
|
|
2257
|
+
const summary = checks.map((check) => `${check.level} ${check.label}: ${check.detail}`).join(" \xB7 ");
|
|
2258
|
+
ctx.ui.notify(`${title}: ${failed.length === 0 ? "ok" : "failed"} \xB7 ${summary}`, failed.length === 0 ? "info" : "error");
|
|
2259
|
+
}
|
|
2260
|
+
function appendOperatorNote(api, note) {
|
|
2261
|
+
api.appendEntry(RIG_WORKFLOW_OPERATOR_NOTE, createWorkflowOperatorNote({ note }));
|
|
2262
|
+
}
|
|
2263
|
+
async function runSessionDoctor(api, ctx) {
|
|
2264
|
+
const checks = await collectDoctorChecks(api, ctx);
|
|
2265
|
+
notifyDoctor(ctx, "Sessions doctor", checks);
|
|
2266
|
+
}
|
|
2267
|
+
async function runCollabDoctor(ctx) {
|
|
2268
|
+
const rigCtx = ctx;
|
|
2269
|
+
const filter = resolveRigIdentityFilter(ctx);
|
|
2270
|
+
const checks = [
|
|
2271
|
+
{ label: "identity-filter", level: filter ? "ok" : "warn", detail: filter ? `${filter.selectedRepo ?? "(repo?)"} / ${filter.namespaceKey ?? "(namespace?)"}` : "unavailable" },
|
|
2272
|
+
{ label: "host-api", level: rigCtx.collab?.startHost || rigCtx.collab?.startCollabHost ? "ok" : "fail", detail: "OMP collab host facade" },
|
|
2273
|
+
{ label: "join-api", level: rigCtx.collab?.join || rigCtx.collab?.joinCollabSession ? "ok" : "fail", detail: "OMP collab join facade" },
|
|
2274
|
+
{ label: "registry-api", level: rigCtx.collab?.listActive || rigCtx.collab?.listActiveCollabSessions || rigCtx.collab?.listSessions ? "ok" : "warn", detail: "OMP live collab registry facade" }
|
|
2275
|
+
];
|
|
2276
|
+
if (filter) {
|
|
2277
|
+
const collabs = await listActiveCollab(ctx, filter);
|
|
2278
|
+
checks.push({ label: "registry-read", level: "ok", detail: `${collabs.length} live collab projection(s)` });
|
|
2279
|
+
}
|
|
2280
|
+
notifyDoctor(ctx, "Collab doctor", checks);
|
|
2281
|
+
}
|
|
2282
|
+
function commandFlowActions(api, ctx, injectedBridge) {
|
|
2283
|
+
return {
|
|
2284
|
+
async act(id) {
|
|
2285
|
+
if (id === "setup:start") {
|
|
2286
|
+
await runRigSetupFlow(api, ctx, injectedBridge);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
if (id === "setup:doctor") {
|
|
2290
|
+
const status = await detectRigStartupStatus(ctx);
|
|
2291
|
+
notifyDoctor(ctx, "Setup doctor", status.checks);
|
|
2292
|
+
if (status.reasons.length > 0)
|
|
2293
|
+
ctx.ui.notify(`Setup needs attention: ${status.reasons.join("; ")}`, "warning");
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (id.startsWith("server:select:")) {
|
|
2297
|
+
const alias = decodeActionValue(id.slice("server:select:".length));
|
|
2298
|
+
await selectServerTarget(ctx, selectedServerBridge(ctx, injectedBridge), alias);
|
|
2299
|
+
ctx.ui.notify(`Selected Rig target ${alias}.`, "info");
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
if (id === "server:add-remote") {
|
|
2303
|
+
await addRemoteTarget(ctx, selectedServerBridge(ctx, injectedBridge));
|
|
2304
|
+
ctx.ui.notify("Rig remote target saved and selected.", "info");
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (id === "server:refresh") {
|
|
2308
|
+
ctx.ui.notify("Server target state refreshed from Rig Cockpit.", "info");
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
if (id === "tasks:refresh") {
|
|
2312
|
+
ctx.ui.notify("Task list refreshed from the selected server target.", "info");
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (id.startsWith("task-detail:dispatch:")) {
|
|
2316
|
+
await dispatchSelectedTask(api, ctx, id, injectedBridge);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
if (id.startsWith("run:")) {
|
|
2320
|
+
await joinRun(ctx, id.slice("run:".length));
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (id.startsWith("run-steer:")) {
|
|
2324
|
+
await steerRun(api, ctx, id.slice("run-steer:".length));
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
if (id.startsWith("run-stop:")) {
|
|
2328
|
+
await steerRun(api, ctx, id.slice("run-stop:".length), true);
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if (id.startsWith("inbox:")) {
|
|
2332
|
+
await resolveInboxRequest(api, ctx, id);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
if (id === "doctor:sessions") {
|
|
2336
|
+
await runSessionDoctor(api, ctx);
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
if (id === "doctor:collab") {
|
|
2340
|
+
await runCollabDoctor(ctx);
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
ctx.ui.notify(`Unknown Rig action: ${id}`, "warning");
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
function projectionUpdatedAt(item) {
|
|
2348
|
+
const workflowUpdated = item.workflow.updatedAt ? Date.parse(item.workflow.updatedAt) : 0;
|
|
2349
|
+
const collabUpdated = item.collab?.updatedAt ? Date.parse(item.collab.updatedAt) : 0;
|
|
2350
|
+
return Math.max(item.session.modified.getTime(), Number.isFinite(workflowUpdated) ? workflowUpdated : 0, Number.isFinite(collabUpdated) ? collabUpdated : 0);
|
|
2351
|
+
}
|
|
2352
|
+
function statusGlyph(status) {
|
|
2353
|
+
const value = status.toLowerCase();
|
|
2354
|
+
if (value === "stale")
|
|
2355
|
+
return "\u25CB";
|
|
2356
|
+
if (value === "running")
|
|
2357
|
+
return "\u25CF";
|
|
2358
|
+
if (value === "completed" || value === "merged")
|
|
2359
|
+
return "\u2713";
|
|
2360
|
+
if (value === "failed")
|
|
2361
|
+
return "\u2717";
|
|
2362
|
+
if (value.includes("needs"))
|
|
2363
|
+
return "!";
|
|
2364
|
+
if (["preparing", "created", "validating", "reviewing", "closing-out", "starting", "adopted"].includes(value))
|
|
2365
|
+
return "\u25D0";
|
|
2366
|
+
return "\xB7";
|
|
2367
|
+
}
|
|
2368
|
+
function sessionLabel(item) {
|
|
2369
|
+
const title = item.session.title || item.session.firstMessage || item.session.id;
|
|
2370
|
+
return `${statusGlyph(sessionValue(item))} ${title}`;
|
|
2371
|
+
}
|
|
2372
|
+
function sessionValue(item) {
|
|
2373
|
+
if (item.collab?.stale)
|
|
2374
|
+
return "stale";
|
|
2375
|
+
if (item.run?.status)
|
|
2376
|
+
return item.run.status;
|
|
2377
|
+
if (item.collab)
|
|
2378
|
+
return "collab";
|
|
2379
|
+
return item.workflow.status?.status ?? item.session.status ?? "session";
|
|
2380
|
+
}
|
|
2381
|
+
function sessionDescription(item) {
|
|
2382
|
+
const id = item.session.id.slice(0, 8);
|
|
2383
|
+
const cwd = item.session.cwd || "(unknown cwd)";
|
|
2384
|
+
const link = item.collab ? collabLinks(item.collab) : "";
|
|
2385
|
+
return link ? `${id} \xB7 ${cwd} \xB7 ${link}` : `${id} \xB7 ${cwd}`;
|
|
2386
|
+
}
|
|
2387
|
+
function collabLinks(collab) {
|
|
2388
|
+
const join = collab.joinLink ? `joinLink ${collab.joinLink}` : "";
|
|
2389
|
+
const web = collab.webLink ? `webLink ${collab.webLink}` : "";
|
|
2390
|
+
return join && web ? `${join} \xB7 ${web}` : join || web || "links unavailable";
|
|
2391
|
+
}
|
|
2392
|
+
function inboxItems(sessions) {
|
|
2393
|
+
const pending = [];
|
|
2394
|
+
for (const session of sessions) {
|
|
2395
|
+
for (const request of session.workflow.inbox)
|
|
2396
|
+
pending.push({ session: session.session, request });
|
|
2397
|
+
}
|
|
2398
|
+
return pending.sort((a, b) => Date.parse(b.request.requestedAt) - Date.parse(a.request.requestedAt));
|
|
2399
|
+
}
|
|
2400
|
+
function findSessionByPath(sessions, sessionPath) {
|
|
2401
|
+
if (!sessionPath)
|
|
2402
|
+
return null;
|
|
2403
|
+
return sessions.find((item) => item.session.path === sessionPath) ?? null;
|
|
2404
|
+
}
|
|
2405
|
+
var RUN_DETAIL_STAGES = [
|
|
2406
|
+
"Connect",
|
|
2407
|
+
"GitHub-sync",
|
|
2408
|
+
"Prepare",
|
|
2409
|
+
"Launch",
|
|
2410
|
+
"Plan",
|
|
2411
|
+
"Implement",
|
|
2412
|
+
"Validate",
|
|
2413
|
+
"Commit",
|
|
2414
|
+
"Open-PR",
|
|
2415
|
+
"Review/CI",
|
|
2416
|
+
"Merge",
|
|
2417
|
+
"Complete/Needs-attention/Failed"
|
|
2418
|
+
];
|
|
2419
|
+
function normalizeStageName(value) {
|
|
2420
|
+
const key = value?.toLowerCase().replace(/[_\s]+/g, "-");
|
|
2421
|
+
switch (key) {
|
|
2422
|
+
case "connect":
|
|
2423
|
+
case "collab-host-started":
|
|
2424
|
+
return "Connect";
|
|
2425
|
+
case "github-sync":
|
|
2426
|
+
case "github":
|
|
2427
|
+
case "running-reflection":
|
|
2428
|
+
return "GitHub-sync";
|
|
2429
|
+
case "prepare":
|
|
2430
|
+
case "preparing":
|
|
2431
|
+
return "Prepare";
|
|
2432
|
+
case "launch":
|
|
2433
|
+
case "running":
|
|
2434
|
+
return "Launch";
|
|
2435
|
+
case "plan":
|
|
2436
|
+
case "planning":
|
|
2437
|
+
return "Plan";
|
|
2438
|
+
case "implement":
|
|
2439
|
+
case "implementation":
|
|
2440
|
+
return "Implement";
|
|
2441
|
+
case "validate":
|
|
2442
|
+
case "validating":
|
|
2443
|
+
return "Validate";
|
|
2444
|
+
case "commit":
|
|
2445
|
+
return "Commit";
|
|
2446
|
+
case "push":
|
|
2447
|
+
case "pr-opened":
|
|
2448
|
+
case "open-pr":
|
|
2449
|
+
return "Open-PR";
|
|
2450
|
+
case "pr-review-merge":
|
|
2451
|
+
case "review":
|
|
2452
|
+
case "review-ci":
|
|
2453
|
+
return "Review/CI";
|
|
2454
|
+
case "merge":
|
|
2455
|
+
return "Merge";
|
|
2456
|
+
case "completed":
|
|
2457
|
+
case "complete":
|
|
2458
|
+
case "needs-attention":
|
|
2459
|
+
case "failed":
|
|
2460
|
+
case "stopped":
|
|
2461
|
+
return "Complete/Needs-attention/Failed";
|
|
2462
|
+
default:
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
function stageRows(session) {
|
|
2467
|
+
const statuses = new Map;
|
|
2468
|
+
const details = new Map;
|
|
2469
|
+
for (const stage of RUN_DETAIL_STAGES)
|
|
2470
|
+
statuses.set(stage, "pending");
|
|
2471
|
+
for (const history of session.run?.statusHistory ?? []) {
|
|
2472
|
+
const stage = normalizeStageName(history.to);
|
|
2473
|
+
if (stage) {
|
|
2474
|
+
statuses.set(stage, history.to === "failed" || history.to === "needs-attention" || history.to === "stopped" ? history.to : "completed");
|
|
2475
|
+
if (history.reason)
|
|
2476
|
+
details.set(stage, history.reason);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
for (const entry of session.timeline ?? []) {
|
|
2480
|
+
const stage = normalizeStageName(entry.stage ?? entry.type);
|
|
2481
|
+
if (!stage)
|
|
2482
|
+
continue;
|
|
2483
|
+
const status = entry.status === "failed" || entry.status === "needs-attention" || entry.status === "stopped" ? entry.status : entry.status === "completed" || entry.status === "complete" ? "completed" : entry.status === "running" || entry.status === "started" ? "running" : "completed";
|
|
2484
|
+
statuses.set(stage, status);
|
|
2485
|
+
if (entry.detail)
|
|
2486
|
+
details.set(stage, entry.detail);
|
|
2487
|
+
}
|
|
2488
|
+
for (const phase of session.run?.closeoutPhases ?? []) {
|
|
2489
|
+
const stage = normalizeStageName(phase.phase);
|
|
2490
|
+
if (!stage)
|
|
2491
|
+
continue;
|
|
2492
|
+
statuses.set(stage, phase.outcome === "failed" ? "failed" : phase.outcome === "completed" ? "completed" : "running");
|
|
2493
|
+
if (phase.detail)
|
|
2494
|
+
details.set(stage, phase.detail);
|
|
2495
|
+
}
|
|
2496
|
+
const terminal = session.run?.status;
|
|
2497
|
+
if (terminal === "completed" || terminal === "needs-attention" || terminal === "failed" || terminal === "stopped") {
|
|
2498
|
+
statuses.set("Complete/Needs-attention/Failed", terminal);
|
|
2499
|
+
}
|
|
2500
|
+
return RUN_DETAIL_STAGES.map((stage) => ({
|
|
2501
|
+
id: `stage:${stage}`,
|
|
2502
|
+
label: stage,
|
|
2503
|
+
currentValue: statuses.get(stage) ?? "pending",
|
|
2504
|
+
heading: true,
|
|
2505
|
+
description: details.get(stage) ?? "rig.run timeline / closeout projection"
|
|
2506
|
+
}));
|
|
2507
|
+
}
|
|
2508
|
+
function runDetailItems(session) {
|
|
2509
|
+
const collab = session.collab;
|
|
2510
|
+
const task = session.workflow.task;
|
|
2511
|
+
const encodedPath = encodeURIComponent(session.session.path);
|
|
2512
|
+
const joinState = collab?.stale ? "stale" : "unavailable";
|
|
2513
|
+
const joinDescription = collab?.stale ? "Stale collab registry entry; BACK refreshes the Runs list before joining." : "No active collab link for this OMP session; BACK refreshes the Runs list after the host is live.";
|
|
2514
|
+
const prUrl = session.run?.record.prUrl;
|
|
2515
|
+
const sourceTask = session.run?.record.sourceTask;
|
|
2516
|
+
const issueUrl = sourceTask && typeof sourceTask === "object" && "url" in sourceTask ? String(sourceTask.url ?? "") : null;
|
|
2517
|
+
return [
|
|
2518
|
+
{ id: "title", label: "Run Detail", currentValue: "", heading: true },
|
|
2519
|
+
{ id: "detail:back", label: "BACK", currentValue: "runs", values: ["runs"], description: "refresh runs and return to the session list" },
|
|
2520
|
+
...task ? [{ id: "run-detail:task", label: "TASK", currentValue: task.taskId, heading: true, description: task.title ?? "selected task" }] : [],
|
|
2521
|
+
isJoinableCollab(collab) ? { id: "detail:join", label: "JOIN", currentValue: "live", values: ["join"], description: collabLinks(collab) } : { id: "detail:join-state", label: "JOIN", currentValue: joinState, heading: true, description: joinDescription },
|
|
2522
|
+
...collab?.webLink ? [{ id: "detail:web", label: "OPEN WEB", currentValue: collab.webLink, heading: true, description: "OMP collab web link" }] : [],
|
|
2523
|
+
{ id: `run-steer:${encodedPath}`, label: "STEER", currentValue: "message", values: ["send"], description: "join this run and send a steering prompt over OMP collab guest transport" },
|
|
2524
|
+
{ id: `run-stop:${encodedPath}`, label: "STOP", currentValue: "graceful", values: ["stop"], description: "send Rig stop sentinel over collab and request host abort" },
|
|
2525
|
+
{ id: "detail:summary", label: "SUMMARY", currentValue: sessionValue(session), heading: true, description: sessionLabel(session) },
|
|
2526
|
+
{ id: "detail:cwd", label: "CWD", currentValue: session.session.cwd || "(unknown cwd)", heading: true, description: session.session.id },
|
|
2527
|
+
{ id: "detail:status", label: "STATUS", currentValue: session.run?.status ?? session.workflow.status?.status ?? session.session.status ?? "session", heading: true, description: session.workflow.status?.detail || sessionDescription(session) },
|
|
2528
|
+
...prUrl ? [{ id: "detail:pr", label: "PR", currentValue: prUrl, heading: true, description: "closeout pull request" }] : [],
|
|
2529
|
+
...issueUrl ? [{ id: "detail:issue", label: "ISSUE", currentValue: issueUrl, heading: true, description: "source task issue" }] : [],
|
|
2530
|
+
{ id: "detail:stages", label: "STAGES", currentValue: "", heading: true, description: "Connect \u2192 GitHub-sync \u2192 Prepare \u2192 Launch \u2192 Plan \u2192 Implement \u2192 Validate \u2192 Commit \u2192 Open-PR \u2192 Review/CI \u2192 Merge \u2192 Complete" },
|
|
2531
|
+
...stageRows(session),
|
|
2532
|
+
{ id: "detail:inbox", label: "INBOX", currentValue: String(session.workflow.inbox.length), heading: true, description: session.workflow.inbox.length > 0 ? "Actionable run inbox items below." : "No pending Rig workflow inbox requests." },
|
|
2533
|
+
...session.workflow.inbox.map((item) => ({
|
|
2534
|
+
id: inboxActionId(session.session, item),
|
|
2535
|
+
label: item.title,
|
|
2536
|
+
currentValue: "resolve",
|
|
2537
|
+
values: ["resolve"],
|
|
2538
|
+
description: item.body || item.kind
|
|
2539
|
+
}))
|
|
2540
|
+
];
|
|
2541
|
+
}
|
|
2542
|
+
function serverLabel(server) {
|
|
2543
|
+
return server?.alias?.trim() || "unselected";
|
|
2544
|
+
}
|
|
2545
|
+
function serverTarget(server) {
|
|
2546
|
+
if (!server)
|
|
2547
|
+
return "unknown";
|
|
2548
|
+
return server.kind === "remote" ? server.baseUrl?.trim() || "remote" : "local";
|
|
2549
|
+
}
|
|
2550
|
+
function nextStepHint(server, sessions) {
|
|
2551
|
+
if (!server)
|
|
2552
|
+
return "next: Server";
|
|
2553
|
+
if (sessions.some((session) => session.run?.status && !["completed", "failed", "stopped"].includes(session.run.status)))
|
|
2554
|
+
return "next: Runs";
|
|
2555
|
+
return "next: Tasks";
|
|
2556
|
+
}
|
|
2557
|
+
function taskRows(tasks, currentUserLogin) {
|
|
2558
|
+
if (!tasks) {
|
|
2559
|
+
return [{ id: "tasks:loading", label: "TASKS", currentValue: "not loaded", heading: true, description: "Refresh reads tasks from the selected spawn target's task source." }];
|
|
2560
|
+
}
|
|
2561
|
+
if (tasks.length === 0) {
|
|
2562
|
+
return [{ id: "tasks:empty", label: "No configured tasks", currentValue: "", heading: true, description: "The selected server task source returned no tasks." }];
|
|
2563
|
+
}
|
|
2564
|
+
const projected = tasks.map(taskBadgeProjection);
|
|
2565
|
+
const visible = process.env.RIG_TASKS_ASSIGNED_TO_ME === "1" ? selectTasksAssignedToMe(projected, currentUserLogin ?? null) : projected;
|
|
2566
|
+
const sourceById = new Map(tasks.map((task) => [task.id, task]));
|
|
2567
|
+
const badgesByTaskId = computeTaskDependencyBadges(visible);
|
|
2568
|
+
const groups = selectTasksGroupedByStatus({ tasks: visible });
|
|
2569
|
+
const next = selectNextReadyTaskByPriority(visible);
|
|
2570
|
+
const ordered = groups.flatMap((group) => group.tasks).slice(0, 20);
|
|
2571
|
+
return ordered.flatMap((task) => {
|
|
2572
|
+
const sourceTask = sourceById.get(task.id);
|
|
2573
|
+
if (!sourceTask)
|
|
2574
|
+
return [];
|
|
2575
|
+
const summary = badgesByTaskId.get(task.id);
|
|
2576
|
+
const badge = summary?.badges.find((entry) => entry.kind === "blocked" || entry.kind === "ready")?.label ?? taskStatus(sourceTask);
|
|
2577
|
+
return [{
|
|
2578
|
+
id: `task-detail:${encodeURIComponent(task.id)}`,
|
|
2579
|
+
label: taskTitle(sourceTask),
|
|
2580
|
+
currentValue: badge,
|
|
2581
|
+
values: ["open"],
|
|
2582
|
+
description: [next?.id === task.id ? "next-ready" : null, taskAssigneeText(sourceTask), summary?.badges.map((entry) => entry.description).join("; "), taskDescription(sourceTask) || task.id].filter(Boolean).join(" \xB7 ")
|
|
2583
|
+
}];
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
function taskDetailRows(state, actionRowsEnabled) {
|
|
2587
|
+
const task = state.selectedTask ?? null;
|
|
2588
|
+
const id = taskId(task);
|
|
2589
|
+
if (!task || !id) {
|
|
2590
|
+
return [
|
|
2591
|
+
{ id: "title", label: "Task Detail", currentValue: "", heading: true },
|
|
2592
|
+
{ id: "detail:back", label: "BACK", currentValue: "tasks", values: ["tasks"], description: "return to the configured task list" },
|
|
2593
|
+
{ id: "task-detail:none", label: "No task selected", currentValue: state.selectedTaskId ?? "", heading: true, description: "Open a task row from Tasks before dispatch." }
|
|
2594
|
+
];
|
|
2595
|
+
}
|
|
2596
|
+
return [
|
|
2597
|
+
{ id: "title", label: "Task Detail", currentValue: "", heading: true },
|
|
2598
|
+
{ id: "detail:back", label: "BACK", currentValue: "tasks", values: ["tasks"], description: "return to the configured task list" },
|
|
2599
|
+
{ id: "task-detail:summary", label: "TASK", currentValue: id, heading: true, description: taskTitle(task) },
|
|
2600
|
+
{ id: "task-detail:status", label: "STATUS", currentValue: taskStatus(task), heading: true, description: task.source ?? "configured task source" },
|
|
2601
|
+
...task.url ? [{ id: "task-detail:url", label: "URL", currentValue: task.url, heading: true, description: "source task URL" }] : [],
|
|
2602
|
+
...task.body ? [{ id: "task-detail:body", label: "BODY", currentValue: task.body.slice(0, 120), heading: true, description: "source task body" }] : [],
|
|
2603
|
+
actionRowsEnabled ? { id: `task-detail:dispatch:${encodeURIComponent(id)}`, label: "DISPATCH", currentValue: state.server?.kind ?? "selected target", values: ["dispatch"], description: "submit the selected task run through the selected server target, then attach OMP collab" } : { id: `task-detail:dispatch:${encodeURIComponent(id)}`, label: "DISPATCH", currentValue: "selected target", values: ["dispatch"], description: "direct dispatch through the selected server target" }
|
|
2604
|
+
];
|
|
2605
|
+
}
|
|
2606
|
+
function itemsFor(screen, sessions, actionRowsEnabled, selectedSessionPath, state = {}) {
|
|
2607
|
+
const selectedSession = findSessionByPath(sessions, selectedSessionPath);
|
|
2608
|
+
const server = state.server ?? null;
|
|
2609
|
+
if (screen === "runs" && selectedSession)
|
|
2610
|
+
return runDetailItems(selectedSession);
|
|
2611
|
+
switch (screen) {
|
|
2612
|
+
case "cockpit":
|
|
2613
|
+
return [
|
|
2614
|
+
{ id: "title", label: "Project Cockpit", currentValue: "", heading: true },
|
|
2615
|
+
{ id: "next", label: "NEXT", currentValue: nextStepHint(server, sessions), heading: true, description: "guided path: Server \u2192 Tasks \u2192 Detail \u2192 Dispatch \u2192 Run Detail" },
|
|
2616
|
+
{ id: "to:server", label: "SERVER", currentValue: serverLabel(server), values: ["open"], description: "select/configure execution placement before listing tasks" },
|
|
2617
|
+
{ id: "to:tasks", label: "TASKS", currentValue: server?.taskSource ?? "configured source", values: ["open"], description: "list tasks from the selected server target's configured task source" },
|
|
2618
|
+
{ id: "to:runs", label: "RUNS", currentValue: String(sessions.length), values: ["open"], description: "attach to dispatched Rig runs and live OMP collaborative sessions" },
|
|
2619
|
+
{ id: "to:inbox", label: "INBOX", currentValue: String(inboxItems(sessions).length), values: ["open"], description: "pending Rig workflow requests recorded in OMP entries" },
|
|
2620
|
+
{ id: "to:doctor", label: "DOCTOR", currentValue: "open", values: ["open"], description: "diagnose session, collaboration, and selected-target seams" },
|
|
2621
|
+
{ id: "to:setup", label: "SETUP", currentValue: "open", values: ["open"], description: "repair project setup, auth, labels, placement, and local OMP extension registration" }
|
|
2622
|
+
];
|
|
2623
|
+
case "runs":
|
|
2624
|
+
return [
|
|
2625
|
+
{ id: "title", label: "Runs", currentValue: sessions.length > 0 ? String(sessions.length) : "", heading: true, description: "dispatched Rig runs; open one to attach its live OMP collaborative session" },
|
|
2626
|
+
...sessions.length > 0 ? sessions.slice(0, 12).map((item) => ({
|
|
2627
|
+
id: `run-detail:${encodeURIComponent(item.session.path)}`,
|
|
2628
|
+
label: sessionLabel(item),
|
|
2629
|
+
currentValue: sessionValue(item),
|
|
2630
|
+
values: ["open"],
|
|
2631
|
+
description: sessionDescription(item)
|
|
2632
|
+
})) : [{ id: "none", label: "No Rig runs discovered", currentValue: "", heading: true, description: "Dispatch a configured task from task detail to create one." }]
|
|
2633
|
+
];
|
|
2634
|
+
case "server":
|
|
2635
|
+
return [
|
|
2636
|
+
{ id: "title", label: "Spawn Target", currentValue: "", heading: true },
|
|
2637
|
+
{ id: "server:selected", label: "TARGET", currentValue: serverLabel(server), heading: true, description: serverTarget(server) },
|
|
2638
|
+
...(state.serverTargets ?? []).map((target) => ({
|
|
2639
|
+
id: `server:select:${encodeURIComponent(target.alias)}`,
|
|
2640
|
+
label: target.alias === server?.alias ? `ACTIVE ${target.alias}` : `SELECT ${target.alias}`,
|
|
2641
|
+
currentValue: target.kind,
|
|
2642
|
+
values: ["select"],
|
|
2643
|
+
description: `${target.status ?? "ready"} \xB7 ${target.projectRoot ?? target.baseUrl ?? "target"} \xB7 ${target.taskSource ?? "tasks"}`
|
|
2644
|
+
})),
|
|
2645
|
+
{ id: "server:placement", label: "PLACEMENT", currentValue: server?.kind ?? "unknown", heading: true, description: "collaboration is invariant OMP session capability, not a target" },
|
|
2646
|
+
{ id: "server:root", label: "PROJECT ROOT", currentValue: server?.projectRoot ?? "(unscoped)", heading: true, description: server?.detail ?? "remote dispatch preserves the spawn target project-root link; local dispatch scopes to this checkout" },
|
|
2647
|
+
{ id: "server:status", label: "STATUS", currentValue: server?.status ?? "unknown", heading: true, description: server?.taskSource ? `task source: ${server.taskSource}` : "spawn target status" },
|
|
2648
|
+
{ id: "server:add-remote", label: "ADD REMOTE", currentValue: "alias", values: ["add"], description: "save a remote alias to remotes.toml/authority and select it" },
|
|
2649
|
+
actionRowsEnabled ? { id: "server:refresh", label: "REFRESH", currentValue: "spawn target", values: ["refresh"], description: "refresh spawn-placement state before reading tasks" } : { id: "server:refresh", label: "REFRESH", currentValue: "spawn target", values: ["refresh"], description: "refresh spawn-placement state before reading tasks" }
|
|
2650
|
+
];
|
|
2651
|
+
case "tasks":
|
|
2652
|
+
return [
|
|
2653
|
+
{ id: "title", label: "Configured Tasks", currentValue: "", heading: true },
|
|
2654
|
+
{ id: "tasks:source", label: "TASKS", currentValue: server?.taskSource ?? "configured source", heading: true, description: `rows come from ${serverLabel(server)} on the selected target; no synthetic tasks` },
|
|
2655
|
+
...taskRows(state.tasks, state.currentUserLogin),
|
|
2656
|
+
actionRowsEnabled ? { id: "tasks:refresh", label: "REFRESH", currentValue: "task source", values: ["refresh"], description: "read configured tasks from the selected target" } : { id: "tasks:refresh", label: "REFRESH", currentValue: "task source", values: ["refresh"], description: "read configured tasks from the selected target" }
|
|
2657
|
+
];
|
|
2658
|
+
case "task-detail":
|
|
2659
|
+
return taskDetailRows({ ...state, server }, actionRowsEnabled);
|
|
2660
|
+
case "inbox": {
|
|
2661
|
+
const pending = inboxItems(sessions);
|
|
2662
|
+
return [
|
|
2663
|
+
{ id: "title", label: "Inbox", currentValue: "", heading: true },
|
|
2664
|
+
...pending.length > 0 ? pending.slice(0, 12).map(({ session, request }) => ({
|
|
2665
|
+
id: inboxActionId(session, request),
|
|
2666
|
+
label: request.title,
|
|
2667
|
+
currentValue: actionRowsEnabled ? "resolve" : "open",
|
|
2668
|
+
values: [actionRowsEnabled ? "resolve" : "open"],
|
|
2669
|
+
description: request.body || `${request.kind} \xB7 ${session.id.slice(0, 8)} \xB7 ${session.cwd || "(unknown cwd)"}`
|
|
2670
|
+
})) : [{ id: "empty", label: "No pending Rig workflow inbox requests", currentValue: "", heading: true, description: `${RIG_WORKFLOW_INBOX_REQUESTED} entries will appear here until ${RIG_WORKFLOW_INBOX_RESOLVED}.` }]
|
|
2671
|
+
];
|
|
2672
|
+
}
|
|
2673
|
+
case "setup": {
|
|
2674
|
+
const status = state.setupStatus;
|
|
2675
|
+
return [
|
|
2676
|
+
{ id: "title", label: "Setup", currentValue: status?.configured ? "configured" : "needs setup", heading: true },
|
|
2677
|
+
{ id: "setup:repo", label: "REPO", currentValue: status?.slug ?? "unknown", heading: true, description: status?.projectRoot ?? "current workspace" },
|
|
2678
|
+
{ id: "setup:config", label: "CONFIG", currentValue: status?.config.valid ? "ok" : "needs repair", heading: true, description: status?.config.reason ?? status?.config.path ?? "rig.config.ts" },
|
|
2679
|
+
{ id: "setup:state", label: "STATE", currentValue: status?.state.valid ? "ok" : "needs repair", heading: true, description: status?.state.reason ?? `${status?.state.selected ?? "placement"} -> ${status?.state.project ?? "project"}` },
|
|
2680
|
+
{ id: "setup:auth", label: "GITHUB AUTH", currentValue: status?.auth.ok ? status.auth.source : "needs auth", heading: true, description: status?.auth.detail ?? "stored token or gh auth required" },
|
|
2681
|
+
{ id: "setup:placement", label: "PLACEMENT", currentValue: process.env.RIG_SSH_TARGET?.trim() ? "remote" : "local", heading: true, description: process.env.RIG_SSH_TARGET?.trim() ? `ssh ${process.env.RIG_SSH_TARGET.trim()} \xB7 set via rig config set sshTarget <t>` : "runs execute locally \xB7 rig config set sshTarget <ubuntu@host> for remote" },
|
|
2682
|
+
{ id: "setup:backbone", label: "BACKBONE", currentValue: rigRelayUrl().replace(/^wss?:\/\//, ""), heading: true, description: `relay ${rigRelayUrl()} \xB7 registry ${registryBaseUrl()}` },
|
|
2683
|
+
...status?.reasons.length ? [{ id: "setup:reasons", label: "NEEDS ATTENTION", currentValue: String(status.reasons.length), heading: true, description: status.reasons.join(" \xB7 ") }] : [],
|
|
2684
|
+
...(state.serverTargets ?? []).map((target) => ({
|
|
2685
|
+
id: `server:select:${encodeURIComponent(target.alias)}`,
|
|
2686
|
+
label: target.alias === server?.alias ? `ACTIVE ${target.alias}` : `PLACEMENT ${target.alias}`,
|
|
2687
|
+
currentValue: target.kind,
|
|
2688
|
+
values: ["select"],
|
|
2689
|
+
description: `${target.status ?? "ready"} \xB7 ${target.projectRoot ?? target.baseUrl ?? "target"}`
|
|
2690
|
+
})),
|
|
2691
|
+
{ id: "setup:start", label: status?.configured ? "RECONFIGURE" : "START SETUP", currentValue: "wizard", values: ["run"], description: "detect repo, choose placement, authenticate GitHub, write state/config, labels, and verify OMP extension" },
|
|
2692
|
+
{ id: "setup:doctor", label: "CHECK SETUP", currentValue: "doctor", values: ["check"], description: "run cheap setup checks without changing files" }
|
|
2693
|
+
];
|
|
2694
|
+
}
|
|
2695
|
+
case "doctor":
|
|
2696
|
+
return [
|
|
2697
|
+
{ id: "title", label: "Doctor", currentValue: "", heading: true },
|
|
2698
|
+
...doctorCheckRows(state.doctorChecks),
|
|
2699
|
+
actionRowsEnabled ? { id: "doctor:sessions", label: "RE-PROBE SESSIONS", currentValue: "probe", values: ["probe"], description: "refresh persistent doctor rows" } : { id: "doctor:sessions", label: "RE-PROBE SESSIONS", currentValue: "probe", values: ["probe"], description: "probe SessionManager visibility directly from Rig chrome" },
|
|
2700
|
+
{ id: "doctor:truth", label: "TRUTH", currentValue: "Rig Cockpit", heading: true, description: "Rig reads selected server/task state and OMP session entries" }
|
|
2701
|
+
];
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
class RigFlowComponent {
|
|
2706
|
+
tui;
|
|
2707
|
+
api;
|
|
2708
|
+
done;
|
|
2709
|
+
onQuit;
|
|
2710
|
+
refreshSessions;
|
|
2711
|
+
refreshState;
|
|
2712
|
+
actions;
|
|
2713
|
+
actionRowsEnabled;
|
|
2714
|
+
#view;
|
|
2715
|
+
#sessions;
|
|
2716
|
+
#state;
|
|
2717
|
+
#error = "";
|
|
2718
|
+
#runsList = new RunsList;
|
|
2719
|
+
#tasksView = new TasksView;
|
|
2720
|
+
#inboxView = new InboxView;
|
|
2721
|
+
#helpView = new HelpView;
|
|
2722
|
+
#serverLabel = "resolving target\u2026";
|
|
2723
|
+
#notice = null;
|
|
2724
|
+
#inputLine = null;
|
|
2725
|
+
#tick = 0;
|
|
2726
|
+
#sessionByRunId = new Map;
|
|
2727
|
+
#inboxPathByKey = new Map;
|
|
2728
|
+
#discoveryFiber;
|
|
2729
|
+
#refreshing = false;
|
|
2730
|
+
constructor(tui, api, done, onQuit, screen, sessions, state, refreshSessions, refreshState, actions, actionRowsEnabled) {
|
|
2731
|
+
this.tui = tui;
|
|
2732
|
+
this.api = api;
|
|
2733
|
+
this.done = done;
|
|
2734
|
+
this.onQuit = onQuit;
|
|
2735
|
+
this.refreshSessions = refreshSessions;
|
|
2736
|
+
this.refreshState = refreshState;
|
|
2737
|
+
this.actions = actions;
|
|
2738
|
+
this.actionRowsEnabled = actionRowsEnabled;
|
|
2739
|
+
this.#sessions = sessions;
|
|
2740
|
+
this.#state = state;
|
|
2741
|
+
this.#view = screen === "tasks" ? "tasks" : screen === "inbox" ? "inbox" : "board";
|
|
2742
|
+
this.#feedViews();
|
|
2743
|
+
this.#startDiscoveryStream();
|
|
2744
|
+
}
|
|
2745
|
+
#startDiscoveryStream() {
|
|
2746
|
+
try {
|
|
2747
|
+
const root = rigProjectRoot();
|
|
2748
|
+
const remote = this.#remoteRunChangesStream(root);
|
|
2749
|
+
const sources = [
|
|
2750
|
+
{ source: "local", stream: localRunChanges(root) },
|
|
2751
|
+
...remote ? [{ source: "remote", stream: remote }] : []
|
|
2752
|
+
];
|
|
2753
|
+
const onChange = () => this.#refreshFromEvent();
|
|
2754
|
+
const program = Effect.gen(function* () {
|
|
2755
|
+
const events = yield* forkDiscoverySources(sources);
|
|
2756
|
+
yield* Stream.runForEach(events.pipe(Stream.debounce(Duration.millis(500))), () => Effect.promise(onChange));
|
|
2757
|
+
}).pipe(Effect.provide(RunDiscoveryBus.layer));
|
|
2758
|
+
this.#discoveryFiber = Effect.runFork(program);
|
|
2759
|
+
} catch {}
|
|
2760
|
+
}
|
|
2761
|
+
#remoteRunChangesStream(projectRoot) {
|
|
2762
|
+
try {
|
|
2763
|
+
if (!resolveSelectedRemote(projectRoot))
|
|
2764
|
+
return null;
|
|
2765
|
+
const secret = registrySecret();
|
|
2766
|
+
const namespaceKey = resolveOwnerNamespaceKey(projectRoot);
|
|
2767
|
+
if (!secret || !namespaceKey)
|
|
2768
|
+
return null;
|
|
2769
|
+
const base = registryBaseUrl().replace(/\/+$/, "");
|
|
2770
|
+
const url = `${base}/subscribe?namespaceKey=${encodeURIComponent(namespaceKey)}`;
|
|
2771
|
+
return remoteRunChanges({ url, token: deriveOwnerToken(namespaceKey, secret) });
|
|
2772
|
+
} catch {
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
async#refreshFromEvent() {
|
|
2777
|
+
if (this.#refreshing)
|
|
2778
|
+
return;
|
|
2779
|
+
this.#refreshing = true;
|
|
2780
|
+
try {
|
|
2781
|
+
await this.#refreshCurrent();
|
|
2782
|
+
this.tui.requestRender();
|
|
2783
|
+
} catch {} finally {
|
|
2784
|
+
this.#refreshing = false;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
#feedViews() {
|
|
2788
|
+
this.#serverLabel = serverLabel(this.#state.server ?? null);
|
|
2789
|
+
this.#sessionByRunId.clear();
|
|
2790
|
+
this.#serverLabel = serverLabel(this.#state.server ?? null);
|
|
2791
|
+
this.#sessionByRunId.clear();
|
|
2792
|
+
const runs = this.#sessions.map((entry) => {
|
|
2793
|
+
const runId = entry.session.id;
|
|
2794
|
+
this.#sessionByRunId.set(runId, entry);
|
|
2795
|
+
return { runId, status: sessionValue(entry), title: entry.session.title || entry.session.firstMessage || entry.session.id };
|
|
2796
|
+
});
|
|
2797
|
+
this.#runsList.setRuns(runs);
|
|
2798
|
+
this.#tasksView.setTasks((this.#state.tasks ?? []).map((task) => ({
|
|
2799
|
+
id: String(task.id),
|
|
2800
|
+
status: typeof task.status === "string" ? task.status : "open",
|
|
2801
|
+
title: typeof task.title === "string" ? task.title : "(untitled)"
|
|
2802
|
+
})));
|
|
2803
|
+
this.#inboxPathByKey.clear();
|
|
2804
|
+
const items = inboxItems(this.#sessions).map((entry) => {
|
|
2805
|
+
const requestId = String(entry.request.requestId ?? "");
|
|
2806
|
+
const runId = entry.session.id;
|
|
2807
|
+
const kind = entry.request.kind === "approval" ? "approval" : "input";
|
|
2808
|
+
this.#inboxPathByKey.set(`${runId}:${requestId}`, entry.session.path);
|
|
2809
|
+
return { kind, runId, requestId, summary: entry.request.title ?? (kind === "approval" ? "approval requested" : "input requested"), questionId: null, options: [] };
|
|
2810
|
+
});
|
|
2811
|
+
this.#inboxView.setItems(items);
|
|
2812
|
+
this.#runsList.tick = this.#tick;
|
|
2813
|
+
this.#tasksView.tick = this.#tick;
|
|
2814
|
+
this.#inboxView.tick = this.#tick;
|
|
2815
|
+
}
|
|
2816
|
+
#activeView() {
|
|
2817
|
+
return this.#view === "tasks" ? this.#tasksView : this.#view === "inbox" ? this.#inboxView : this.#runsList;
|
|
2818
|
+
}
|
|
2819
|
+
#setView(next) {
|
|
2820
|
+
this.#view = next;
|
|
2821
|
+
this.#notice = null;
|
|
2822
|
+
this.tui.requestRender();
|
|
2823
|
+
}
|
|
2824
|
+
async#refreshCurrent() {
|
|
2825
|
+
this.#sessions = await this.refreshSessions();
|
|
2826
|
+
const screen = this.#view === "tasks" ? "tasks" : this.#view === "inbox" ? "inbox" : "runs";
|
|
2827
|
+
this.#state = await this.refreshState(screen, null);
|
|
2828
|
+
if (this.#state.error)
|
|
2829
|
+
this.#error = this.#state.error;
|
|
2830
|
+
this.#feedViews();
|
|
2831
|
+
}
|
|
2832
|
+
async#runAction(action) {
|
|
2833
|
+
try {
|
|
2834
|
+
this.#error = "";
|
|
2835
|
+
await action();
|
|
2836
|
+
await this.#refreshCurrent();
|
|
2837
|
+
this.tui.requestRender();
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
this.#error = error instanceof Error ? error.message : String(error);
|
|
2840
|
+
this.tui.requestRender();
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
#headerLine() {
|
|
2844
|
+
const crumb = this.#view === "help" ? ` ${ink4("\u203A")} ${ink2("help")}` : this.#view === "tasks" ? ` ${ink4("\u203A")} ${ink2("new run")}` : this.#view === "inbox" ? ` ${ink4("\u203A")} ${ink2("inbox")}` : "";
|
|
2845
|
+
return ` ${accent("\u258D")}${bold(ink("rig"))} ${ink3("\u2014 operate the drones")}${crumb} ${ink4("\xB7")} ${cyan(this.#serverLabel)}`;
|
|
2846
|
+
}
|
|
2847
|
+
#footerLines() {
|
|
2848
|
+
if (this.#inputLine) {
|
|
2849
|
+
const label = this.#inputLine.kind === "search" ? `${accent("/")}${ink(this.#inputLine.buffer)}` : `${accentDim("answer")} ${ink3(`(${this.#inputLine.item.runId.slice(0, 8)})`)} ${accent("\u276F")} ${ink(this.#inputLine.buffer)}`;
|
|
2850
|
+
return [
|
|
2851
|
+
` ${label}${accent("\u2588")}`,
|
|
2852
|
+
` ${accent("enter")}${ink3(this.#inputLine.kind === "search" ? " keep filter" : " send answer")} ${accent("esc")}${ink3(this.#inputLine.kind === "search" ? " clear" : " cancel")}`
|
|
2853
|
+
];
|
|
2854
|
+
}
|
|
2855
|
+
if (this.#error)
|
|
2856
|
+
return [` ${red(`error: ${this.#error}`)}`, ` ${accent("esc")}${ink3(" board")} ${accent("q")}${ink3(" quit")}`];
|
|
2857
|
+
if (this.#view === "help")
|
|
2858
|
+
return [` ${accent("\u2190")}${ink3(" back")} ${accent("esc")}${ink3(" board")} ${accent("q")}${ink3(" quit")}`];
|
|
2859
|
+
if (this.#view === "tasks") {
|
|
2860
|
+
return [
|
|
2861
|
+
` ${this.#notice ? accentDim(this.#notice) : ink4("pick a task \u2014 a drone launches with project defaults")}`,
|
|
2862
|
+
` ${accent("\u2190")}${ink3(" back")} ${accent("enter")}${ink3(" dispatch")} ${accent("r")}${ink3(" reload")} ${accent("esc")}${ink3(" board")} ${accent("q")}${ink3(" quit")}`
|
|
2863
|
+
];
|
|
2864
|
+
}
|
|
2865
|
+
if (this.#view === "inbox") {
|
|
2866
|
+
return [
|
|
2867
|
+
` ${this.#notice ? accentDim(this.#notice) : ink4("gates the fleet is waiting on \u2014 resolve them here")}`,
|
|
2868
|
+
` ${accent("\u2190")}${ink3(" back")} ${accent("a")}${ink3(" approve")} ${accent("d")}${ink3(" reject")} ${accent("enter")}${ink3(" answer input")} ${accent("r")}${ink3(" reload")} ${accent("esc")}${ink3(" board")}`
|
|
2869
|
+
];
|
|
2870
|
+
}
|
|
2871
|
+
const pending = this.#inboxView.items.length;
|
|
2872
|
+
const status = this.#notice ?? (pending > 0 ? `${RIG_SPINNER_FRAMES[this.#tick % RIG_SPINNER_FRAMES.length]} ${pending} inbox gate${pending === 1 ? "" : "s"} waiting \u2014 press i` : "fleet idle gates clear");
|
|
2873
|
+
return [
|
|
2874
|
+
` ${pending > 0 && !this.#notice ? accentDim(status) : ink4(status)}`,
|
|
2875
|
+
` ${accent("enter")}${ink3(" attach")} ${accent("n")}${ink3(" new run")} ${accent("i")}${ink3(" inbox")} ${accent("/")}${ink3(" search")} ${accent("o")}${ink3(" sort")} ${accent("x")}${ink3(" stop")} ${accent("?")}${ink3(" help")} ${accent("q")}${ink3(" quit")}`
|
|
2876
|
+
];
|
|
2877
|
+
}
|
|
2878
|
+
async#attach(run) {
|
|
2879
|
+
const session = this.#sessionByRunId.get(run.runId);
|
|
2880
|
+
if (!session)
|
|
2881
|
+
return;
|
|
2882
|
+
await this.#runAction(() => this.actions.act(`run:${encodeURIComponent(session.session.path)}`));
|
|
2883
|
+
if (!this.#error)
|
|
2884
|
+
this.done();
|
|
2885
|
+
}
|
|
2886
|
+
async#dispatch(task) {
|
|
2887
|
+
this.#notice = `dispatching drone for ${task.id}\u2026`;
|
|
2888
|
+
this.tui.requestRender();
|
|
2889
|
+
await this.#runAction(() => this.actions.act(`task-detail:dispatch:${encodeURIComponent(task.id)}`));
|
|
2890
|
+
this.#setView("board");
|
|
2891
|
+
this.#notice = `drone dispatched on ${task.id}`;
|
|
2892
|
+
}
|
|
2893
|
+
async#stop(run) {
|
|
2894
|
+
const session = this.#sessionByRunId.get(run.runId);
|
|
2895
|
+
if (!session)
|
|
2896
|
+
return;
|
|
2897
|
+
this.#notice = `stop requested for ${run.runId.slice(0, 8)}\u2026`;
|
|
2898
|
+
this.tui.requestRender();
|
|
2899
|
+
await this.#runAction(() => this.actions.act(`run-stop:${encodeURIComponent(session.session.path)}`));
|
|
2900
|
+
}
|
|
2901
|
+
async#resolveInbox(item) {
|
|
2902
|
+
const path = this.#inboxPathByKey.get(`${item.runId}:${item.requestId}`);
|
|
2903
|
+
if (!path)
|
|
2904
|
+
return;
|
|
2905
|
+
await this.#runAction(() => this.actions.act(`inbox:${encodeURIComponent(path)}:${encodeURIComponent(item.requestId)}`));
|
|
2906
|
+
}
|
|
2907
|
+
render(width) {
|
|
2908
|
+
const lines = [this.#headerLine(), hairline(width)];
|
|
2909
|
+
const view = this.#view === "help" ? this.#helpView : this.#activeView();
|
|
2910
|
+
for (const line of view.render(width))
|
|
2911
|
+
lines.push(line);
|
|
2912
|
+
lines.push(hairline(width));
|
|
2913
|
+
for (const line of this.#footerLines())
|
|
2914
|
+
lines.push(line);
|
|
2915
|
+
return lines;
|
|
2916
|
+
}
|
|
2917
|
+
invalidate() {
|
|
2918
|
+
this.#runsList.invalidate();
|
|
2919
|
+
this.#tasksView.invalidate();
|
|
2920
|
+
this.#inboxView.invalidate();
|
|
2921
|
+
this.#helpView.invalidate();
|
|
2922
|
+
}
|
|
2923
|
+
handleInput(data) {
|
|
2924
|
+
if (data === "q") {
|
|
2925
|
+
this.onQuit();
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
if (isKeyRelease(data))
|
|
2929
|
+
return;
|
|
2930
|
+
if (this.#inputLine) {
|
|
2931
|
+
const line = this.#inputLine;
|
|
2932
|
+
if (matchesKey(data, "escape")) {
|
|
2933
|
+
if (line.kind === "search")
|
|
2934
|
+
this.#runsList.setFilter("");
|
|
2935
|
+
this.#inputLine = null;
|
|
2936
|
+
} else if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
2937
|
+
this.#inputLine = null;
|
|
2938
|
+
if (line.kind === "answer" && line.buffer.trim())
|
|
2939
|
+
this.#resolveInbox(line.item);
|
|
2940
|
+
} else if (matchesKey(data, "backspace")) {
|
|
2941
|
+
const buffer = line.buffer.slice(0, -1);
|
|
2942
|
+
this.#inputLine = line.kind === "search" ? { kind: "search", buffer } : { kind: "answer", buffer, item: line.item };
|
|
2943
|
+
if (line.kind === "search")
|
|
2944
|
+
this.#runsList.setFilter(buffer);
|
|
2945
|
+
} else if (data.length === 1 && data >= " ") {
|
|
2946
|
+
const buffer = line.buffer + data;
|
|
2947
|
+
this.#inputLine = line.kind === "search" ? { kind: "search", buffer } : { kind: "answer", buffer, item: line.item };
|
|
2948
|
+
if (line.kind === "search")
|
|
2949
|
+
this.#runsList.setFilter(buffer);
|
|
2950
|
+
}
|
|
2951
|
+
this.tui.requestRender();
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
2955
|
+
this.done();
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
if (this.#view === "help") {
|
|
2959
|
+
if (matchesKey(data, "up") || data === "k")
|
|
2960
|
+
this.#helpView.scroll(-1);
|
|
2961
|
+
else if (matchesKey(data, "down") || data === "j")
|
|
2962
|
+
this.#helpView.scroll(1);
|
|
2963
|
+
else if (matchesKey(data, "pageUp"))
|
|
2964
|
+
this.#helpView.scroll(-15);
|
|
2965
|
+
else if (matchesKey(data, "pageDown") || data === " ")
|
|
2966
|
+
this.#helpView.scroll(15);
|
|
2967
|
+
else if (matchesKey(data, "left") || matchesKey(data, "escape") || data === "?")
|
|
2968
|
+
this.#setView("board");
|
|
2969
|
+
this.tui.requestRender();
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
if (this.#view === "tasks") {
|
|
2973
|
+
if (matchesKey(data, "up") || data === "k")
|
|
2974
|
+
this.#tasksView.moveSelection(-1);
|
|
2975
|
+
else if (matchesKey(data, "down") || data === "j")
|
|
2976
|
+
this.#tasksView.moveSelection(1);
|
|
2977
|
+
else if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
2978
|
+
const task = this.#tasksView.selectedTask();
|
|
2979
|
+
if (task)
|
|
2980
|
+
this.#dispatch(task);
|
|
2981
|
+
} else if (data === "r")
|
|
2982
|
+
this.#runAction(async () => {});
|
|
2983
|
+
else if (matchesKey(data, "left") || matchesKey(data, "escape"))
|
|
2984
|
+
this.#setView("board");
|
|
2985
|
+
this.tui.requestRender();
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
if (this.#view === "inbox") {
|
|
2989
|
+
if (matchesKey(data, "up") || data === "k")
|
|
2990
|
+
this.#inboxView.moveSelection(-1);
|
|
2991
|
+
else if (matchesKey(data, "down") || data === "j")
|
|
2992
|
+
this.#inboxView.moveSelection(1);
|
|
2993
|
+
else if (data === "a") {
|
|
2994
|
+
const item = this.#inboxView.selectedItem();
|
|
2995
|
+
if (item?.kind === "approval")
|
|
2996
|
+
this.#resolveInbox(item);
|
|
2997
|
+
} else if (data === "d") {
|
|
2998
|
+
const item = this.#inboxView.selectedItem();
|
|
2999
|
+
if (item?.kind === "approval")
|
|
3000
|
+
this.#resolveInbox(item);
|
|
3001
|
+
} else if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
3002
|
+
const item = this.#inboxView.selectedItem();
|
|
3003
|
+
if (item?.kind === "input")
|
|
3004
|
+
this.#inputLine = { kind: "answer", buffer: "", item };
|
|
3005
|
+
else if (item?.kind === "approval")
|
|
3006
|
+
this.#resolveInbox(item);
|
|
3007
|
+
} else if (data === "r")
|
|
3008
|
+
this.#runAction(async () => {});
|
|
3009
|
+
else if (matchesKey(data, "left") || matchesKey(data, "escape"))
|
|
3010
|
+
this.#setView("board");
|
|
3011
|
+
this.tui.requestRender();
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
3015
|
+
this.#runsList.moveSelection(-1);
|
|
3016
|
+
this.tui.requestRender();
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
3020
|
+
this.#runsList.moveSelection(1);
|
|
3021
|
+
this.tui.requestRender();
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
3025
|
+
const run = this.#runsList.selectedRun();
|
|
3026
|
+
if (run)
|
|
3027
|
+
this.#attach(run);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
if (data === "?" || data === "h") {
|
|
3031
|
+
this.#setView("help");
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
if (data === "n") {
|
|
3035
|
+
this.#setView("tasks");
|
|
3036
|
+
this.#runAction(async () => {});
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
if (data === "i") {
|
|
3040
|
+
this.#setView("inbox");
|
|
3041
|
+
this.#runAction(async () => {});
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
if (data === "/") {
|
|
3045
|
+
this.#inputLine = { kind: "search", buffer: this.#runsList.filter };
|
|
3046
|
+
this.tui.requestRender();
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
if (data === "o") {
|
|
3050
|
+
const mode = this.#runsList.cycleSort();
|
|
3051
|
+
this.#notice = `sorted: ${mode}`;
|
|
3052
|
+
this.tui.requestRender();
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
if (data === "x") {
|
|
3056
|
+
const run = this.#runsList.selectedRun();
|
|
3057
|
+
if (run)
|
|
3058
|
+
this.#stop(run);
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
if (data === "r") {
|
|
3062
|
+
this.#runAction(async () => {});
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
if (matchesKey(data, "escape")) {
|
|
3066
|
+
if (this.#runsList.filter) {
|
|
3067
|
+
this.#runsList.setFilter("");
|
|
3068
|
+
this.tui.requestRender();
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
this.done();
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
dispose() {
|
|
3076
|
+
const fiber = this.#discoveryFiber;
|
|
3077
|
+
this.#discoveryFiber = undefined;
|
|
3078
|
+
if (fiber)
|
|
3079
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
var EMPTY_SCENE_LINES = [];
|
|
3083
|
+
|
|
3084
|
+
class FullscreenSceneProxy {
|
|
3085
|
+
inner;
|
|
3086
|
+
onDispose;
|
|
3087
|
+
constructor(inner, onDispose) {
|
|
3088
|
+
this.inner = inner;
|
|
3089
|
+
this.onDispose = onDispose;
|
|
3090
|
+
}
|
|
3091
|
+
render() {
|
|
3092
|
+
return EMPTY_SCENE_LINES;
|
|
3093
|
+
}
|
|
3094
|
+
handleInput(data) {
|
|
3095
|
+
this.inner.handleInput?.(data);
|
|
3096
|
+
}
|
|
3097
|
+
invalidate() {
|
|
3098
|
+
this.inner.invalidate?.();
|
|
3099
|
+
}
|
|
3100
|
+
dispose() {
|
|
3101
|
+
this.onDispose();
|
|
3102
|
+
this.inner.dispose?.();
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
async function openRigFlow(api, ctx, screen, actions = defaultFlowActions(api, ctx), actionRowsEnabled = true, injectedBridge, quitExits = false) {
|
|
3106
|
+
const refreshSessions = async () => discoverRigSessions(ctx);
|
|
3107
|
+
const refreshState = (nextScreen, selectedTaskId) => loadRigFlowState(ctx, nextScreen, selectedTaskId, injectedBridge, api);
|
|
3108
|
+
const [sessions, state] = await Promise.all([refreshSessions(), refreshState(screen, null)]);
|
|
3109
|
+
await ctx.ui.custom((tui, _theme, _keybindings, done) => {
|
|
3110
|
+
let handle;
|
|
3111
|
+
const hideScene = () => {
|
|
3112
|
+
handle?.hide();
|
|
3113
|
+
handle = undefined;
|
|
3114
|
+
};
|
|
3115
|
+
const finish = () => {
|
|
3116
|
+
hideScene();
|
|
3117
|
+
done(undefined);
|
|
3118
|
+
};
|
|
3119
|
+
const onQuit = quitExits ? () => {
|
|
3120
|
+
hideScene();
|
|
3121
|
+
ctx.shutdown();
|
|
3122
|
+
} : finish;
|
|
3123
|
+
const component = new RigFlowComponent(tui, api, finish, onQuit, screen, sessions, state, refreshSessions, refreshState, actions, actionRowsEnabled);
|
|
3124
|
+
handle = tui.showOverlay(component, { width: "100%", maxHeight: "100%", anchor: "top-left", margin: 0, fullscreen: true });
|
|
3125
|
+
tui.setFocus(component);
|
|
3126
|
+
return new FullscreenSceneProxy(component, hideScene);
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
function launchRigFlow(api, ctx, screen, actions = defaultFlowActions(api, ctx), actionRowsEnabled = true, injectedBridge, quitExits = false) {
|
|
3130
|
+
openRigFlow(api, ctx, screen, actions, actionRowsEnabled, injectedBridge, quitExits).catch((error) => {
|
|
3131
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
function registerWorkflowInboxTool(api) {
|
|
3135
|
+
const { Type } = api.typebox;
|
|
3136
|
+
const parameters = Type.Object({
|
|
3137
|
+
kind: Type.Union([Type.Literal("approval"), Type.Literal("input")], { description: "Whether the operator must approve/reject or provide input." }),
|
|
3138
|
+
title: Type.String({ minLength: 1, description: "Short title shown in the Rig Inbox row." }),
|
|
3139
|
+
body: Type.Optional(Type.String({ description: "Optional context shown before resolving the inbox request." })),
|
|
3140
|
+
options: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { description: "Optional answer choices for input requests." })),
|
|
3141
|
+
requestId: Type.Optional(Type.String({ minLength: 1, description: "Stable workflow request id. Defaults to the tool call id." }))
|
|
3142
|
+
}, { additionalProperties: false });
|
|
3143
|
+
api.registerTool({
|
|
3144
|
+
name: "rig_workflow_inbox_request",
|
|
3145
|
+
label: "Request Rig workflow inbox action",
|
|
3146
|
+
description: "Create a pending Rig workflow inbox request backed by OMP custom entries. Use this when a Rig workflow needs operator approval or input before continuing.",
|
|
3147
|
+
parameters,
|
|
3148
|
+
approval: "write",
|
|
3149
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
3150
|
+
const input = params;
|
|
3151
|
+
const requestId = input.requestId?.trim() || toolCallId;
|
|
3152
|
+
const title = input.title.trim();
|
|
3153
|
+
const body = input.body?.trim();
|
|
3154
|
+
const options = input.options?.map((option) => option.trim()).filter((option) => option.length > 0);
|
|
3155
|
+
const request = createWorkflowInboxRequested({
|
|
3156
|
+
requestId,
|
|
3157
|
+
kind: input.kind,
|
|
3158
|
+
title,
|
|
3159
|
+
...body ? { body } : {},
|
|
3160
|
+
...options && options.length > 0 ? { options } : {}
|
|
3161
|
+
});
|
|
3162
|
+
api.appendEntry(RIG_WORKFLOW_INBOX_REQUESTED, request);
|
|
3163
|
+
appendStatus(api, input.kind === "approval" ? "waiting-approval" : "waiting-input", `Rig workflow inbox request ${requestId}: ${title}`);
|
|
3164
|
+
if (ctx.hasUI)
|
|
3165
|
+
ctx.ui.notify(`Rig inbox request queued: ${title}`, "info");
|
|
3166
|
+
return {
|
|
3167
|
+
content: [{ type: "text", text: `Rig workflow inbox request ${requestId} is pending in the Rig Inbox.` }],
|
|
3168
|
+
details: { requestId, kind: input.kind }
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
async function createRunJournal(sessionManager, runId) {
|
|
3174
|
+
try {
|
|
3175
|
+
const mod = await import("@rig/runtime/control-plane/run-session-writer");
|
|
3176
|
+
const loadedModule = mod;
|
|
3177
|
+
const maybeJournal = loadedModule && typeof loadedModule === "object" && "RunSessionJournal" in loadedModule ? loadedModule.RunSessionJournal : null;
|
|
3178
|
+
const Journal = typeof maybeJournal === "function" ? maybeJournal : null;
|
|
3179
|
+
return Journal ? new Journal(sessionManager, runId) : null;
|
|
3180
|
+
} catch (error) {
|
|
3181
|
+
console.warn(`[rig-run] RunSessionJournal unavailable; run-state arming deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
3182
|
+
return null;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
function appendRunStallDetected(journal, detail = RUN_PROCESS_STALL_DETAIL) {
|
|
3186
|
+
if (!journal)
|
|
3187
|
+
return false;
|
|
3188
|
+
try {
|
|
3189
|
+
journal.appendStall({ detail });
|
|
3190
|
+
return true;
|
|
3191
|
+
} catch (error) {
|
|
3192
|
+
console.warn(`[rig-run] stall-detected append failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3193
|
+
return false;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
function startRunProcessStallMonitor(opts) {
|
|
3197
|
+
if (!opts.journal)
|
|
3198
|
+
return () => {};
|
|
3199
|
+
let stallDetected = opts.alreadyStalled === true;
|
|
3200
|
+
const thresholdMs = opts.thresholdMs ?? TRACKED_RUN_STALL_MS;
|
|
3201
|
+
const now = opts.now ?? (() => Date.now());
|
|
3202
|
+
const timer = setInterval(() => {
|
|
3203
|
+
if (stallDetected)
|
|
3204
|
+
return;
|
|
3205
|
+
if (!computeRunStall({ lastActivityAt: opts.lastActivityAt(), now: now(), thresholdMs }))
|
|
3206
|
+
return;
|
|
3207
|
+
stallDetected = true;
|
|
3208
|
+
appendRunStallDetected(opts.journal);
|
|
3209
|
+
}, opts.intervalMs ?? RUN_PROCESS_STALL_SWEEP_MS);
|
|
3210
|
+
if (typeof timer.unref === "function")
|
|
3211
|
+
timer.unref();
|
|
3212
|
+
return () => clearInterval(timer);
|
|
3213
|
+
}
|
|
3214
|
+
function createRunProcessStopController(journal, stopHost, exit = (code) => {
|
|
3215
|
+
process.exitCode = code;
|
|
3216
|
+
setTimeout(() => process.exit(code), 0);
|
|
3217
|
+
}, deps = {}) {
|
|
3218
|
+
let stopRequested = null;
|
|
3219
|
+
let finished = false;
|
|
3220
|
+
const finishStopped = async () => {
|
|
3221
|
+
if (finished)
|
|
3222
|
+
return;
|
|
3223
|
+
finished = true;
|
|
3224
|
+
journal?.appendTimeline({ type: "stopped", stage: "stopped", status: "completed", detail: stopRequested?.reason ?? "operator requested stop" });
|
|
3225
|
+
journal?.appendStatus("stopped", { actor: { kind: "operator" }, reason: stopRequested?.reason ?? "operator requested stop", force: true });
|
|
3226
|
+
await stopHost?.("rig stop requested").catch(() => {
|
|
3227
|
+
return;
|
|
3228
|
+
});
|
|
3229
|
+
exit(0);
|
|
3230
|
+
};
|
|
3231
|
+
return {
|
|
3232
|
+
isStopRequested: () => Boolean(stopRequested),
|
|
3233
|
+
requestStop: (reason) => {
|
|
3234
|
+
if (stopRequested)
|
|
3235
|
+
return;
|
|
3236
|
+
stopRequested = { reason };
|
|
3237
|
+
journal?.appendTimeline({ type: "stop-requested", stage: "stopped", status: "running", detail: reason ?? "operator requested stop" });
|
|
3238
|
+
if (deps.isIdle?.()) {
|
|
3239
|
+
finishStopped();
|
|
3240
|
+
} else {
|
|
3241
|
+
deps.interrupt?.();
|
|
3242
|
+
}
|
|
3243
|
+
},
|
|
3244
|
+
finishStopped
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
function createRunProcessStopHandlers(runId, stopController) {
|
|
3248
|
+
const detectStopText = (text) => {
|
|
3249
|
+
const stop = parseStopSentinel(text, runId);
|
|
3250
|
+
if (stop)
|
|
3251
|
+
stopController.requestStop(stop.reason);
|
|
3252
|
+
};
|
|
3253
|
+
return {
|
|
3254
|
+
beforeAgentStart: (event) => detectStopText(event.prompt),
|
|
3255
|
+
messageStart: (event) => detectStopText(messageText(event.message))
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
function createRunProcessSteerDriver(api, journal, timeoutMs = RUN_PROCESS_STEER_TIMEOUT_MS) {
|
|
3259
|
+
let pendingSteer = null;
|
|
3260
|
+
const resolvePending = (reason) => {
|
|
3261
|
+
const pending = pendingSteer;
|
|
3262
|
+
if (!pending)
|
|
3263
|
+
return false;
|
|
3264
|
+
pendingSteer = null;
|
|
3265
|
+
clearTimeout(pending.timer);
|
|
3266
|
+
if (reason === "timeout")
|
|
3267
|
+
journal?.appendTimeline({ type: "closeout-steer-timeout", timeoutMs });
|
|
3268
|
+
pending.resolve();
|
|
3269
|
+
return true;
|
|
3270
|
+
};
|
|
3271
|
+
return {
|
|
3272
|
+
steerPi: async (message) => {
|
|
3273
|
+
if (pendingSteer)
|
|
3274
|
+
throw new Error("A run closeout steer turn is already pending.");
|
|
3275
|
+
await new Promise((resolve2, reject) => {
|
|
3276
|
+
const timer = setTimeout(() => {
|
|
3277
|
+
resolvePending("timeout");
|
|
3278
|
+
}, timeoutMs);
|
|
3279
|
+
pendingSteer = { resolve: resolve2, timer };
|
|
3280
|
+
try {
|
|
3281
|
+
journal?.appendTimeline({ type: "closeout-steer", message });
|
|
3282
|
+
api.sendUserMessage(message, { deliverAs: "steer" });
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
pendingSteer = null;
|
|
3285
|
+
clearTimeout(timer);
|
|
3286
|
+
reject(error);
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
},
|
|
3290
|
+
resolvePendingSteerOnAgentEnd: () => resolvePending("agent_end")
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
function messageText(value) {
|
|
3294
|
+
if (typeof value === "string")
|
|
3295
|
+
return value;
|
|
3296
|
+
if (Array.isArray(value))
|
|
3297
|
+
return value.map(messageText).join(`
|
|
3298
|
+
`);
|
|
3299
|
+
if (!isRecord(value))
|
|
3300
|
+
return "";
|
|
3301
|
+
if (typeof value.text === "string")
|
|
3302
|
+
return value.text;
|
|
3303
|
+
if (typeof value.content === "string")
|
|
3304
|
+
return value.content;
|
|
3305
|
+
return messageText(value.content);
|
|
3306
|
+
}
|
|
3307
|
+
function readOperatorSteerTexts(sessionFile) {
|
|
3308
|
+
let raw;
|
|
3309
|
+
try {
|
|
3310
|
+
raw = readFileSync(sessionFile, "utf8");
|
|
3311
|
+
} catch {
|
|
3312
|
+
return [];
|
|
3313
|
+
}
|
|
3314
|
+
const out = [];
|
|
3315
|
+
for (const line of raw.split(`
|
|
3316
|
+
`)) {
|
|
3317
|
+
if (!line.trim())
|
|
3318
|
+
continue;
|
|
3319
|
+
let parsed;
|
|
3320
|
+
try {
|
|
3321
|
+
parsed = JSON.parse(line);
|
|
3322
|
+
} catch {
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
if (!parsed || typeof parsed !== "object" || !("customType" in parsed) || parsed.customType !== RIG_RUN_STEERING)
|
|
3326
|
+
continue;
|
|
3327
|
+
const data = "data" in parsed && parsed.data && typeof parsed.data === "object" ? parsed.data : null;
|
|
3328
|
+
if (!data)
|
|
3329
|
+
continue;
|
|
3330
|
+
const actor = "actor" in data && data.actor && typeof data.actor === "object" ? data.actor : null;
|
|
3331
|
+
const actorKind = actor && "kind" in actor && typeof actor.kind === "string" ? actor.kind : null;
|
|
3332
|
+
if (actorKind !== "operator")
|
|
3333
|
+
continue;
|
|
3334
|
+
const text = "text" in data && typeof data.text === "string" ? data.text : null;
|
|
3335
|
+
if (text)
|
|
3336
|
+
out.push(text);
|
|
3337
|
+
}
|
|
3338
|
+
return out;
|
|
3339
|
+
}
|
|
3340
|
+
async function applyOperatorSteers(opts) {
|
|
3341
|
+
const steers = readOperatorSteerTexts(opts.sessionFile);
|
|
3342
|
+
for (let index = opts.appliedCount;index < steers.length; index += 1) {
|
|
3343
|
+
const stop = parseStopSentinel(steers[index], opts.runId);
|
|
3344
|
+
if (!stop)
|
|
3345
|
+
continue;
|
|
3346
|
+
try {
|
|
3347
|
+
opts.requestStop(stop.reason);
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
console.warn(`[rig-run] stop apply failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
let applied = opts.appliedCount;
|
|
3353
|
+
while (applied < steers.length) {
|
|
3354
|
+
const text = steers[applied];
|
|
3355
|
+
applied += 1;
|
|
3356
|
+
if (parseStopSentinel(text, opts.runId))
|
|
3357
|
+
continue;
|
|
3358
|
+
try {
|
|
3359
|
+
await opts.steerPi(text);
|
|
3360
|
+
} catch (error) {
|
|
3361
|
+
console.warn(`[rig-run] steer apply failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
return applied;
|
|
3365
|
+
}
|
|
3366
|
+
function startOperatorSteerConsumer(opts) {
|
|
3367
|
+
let applied = readOperatorSteerTexts(opts.sessionFile).length;
|
|
3368
|
+
const apply = async () => {
|
|
3369
|
+
applied = await applyOperatorSteers({
|
|
3370
|
+
sessionFile: opts.sessionFile,
|
|
3371
|
+
runId: opts.runId,
|
|
3372
|
+
appliedCount: applied,
|
|
3373
|
+
requestStop: opts.requestStop,
|
|
3374
|
+
steerPi: opts.steerPi
|
|
3375
|
+
});
|
|
3376
|
+
};
|
|
3377
|
+
Effect.runFork(Stream.runForEach(localRunChanges(opts.projectRoot).pipe(Stream.debounce(Duration.millis(500))), () => Effect.promise(apply)));
|
|
3378
|
+
}
|
|
3379
|
+
async function dispatchRunNotifications(projectRoot, runId, taskId2, outcome, detail) {
|
|
3380
|
+
try {
|
|
3381
|
+
const config = await loadNotificationConfig(resolve(projectRoot, ".rig", "notifications.json"));
|
|
3382
|
+
if (config.targets.length === 0)
|
|
3383
|
+
return;
|
|
3384
|
+
const event = { runId, type: `run.${outcome}`, timestamp: new Date().toISOString(), payload: { taskId: taskId2, detail } };
|
|
3385
|
+
await Promise.race([
|
|
3386
|
+
dispatchEventToTargets(event, config.targets),
|
|
3387
|
+
new Promise((resolveCap) => setTimeout(resolveCap, 5000))
|
|
3388
|
+
]);
|
|
3389
|
+
} catch {}
|
|
3390
|
+
}
|
|
3391
|
+
async function maybeStartRunProcessAutohost(api, ctx) {
|
|
3392
|
+
if (process.env.RIG_RUN_PROCESS !== "1")
|
|
3393
|
+
return false;
|
|
3394
|
+
const envRunId = process.env.RIG_RUN_ID?.trim();
|
|
3395
|
+
if (!envRunId)
|
|
3396
|
+
throw new Error("RIG_RUN_ID is required when RIG_RUN_PROCESS=1");
|
|
3397
|
+
const runId = sessionIdFromSessionFile(ctx.sessionManager.getSessionFile()) ?? envRunId;
|
|
3398
|
+
console.log(`[rig-run] session_start hasUI=${ctx.hasUI ? "true" : "false"}`);
|
|
3399
|
+
if (!ctx.hasUI)
|
|
3400
|
+
return true;
|
|
3401
|
+
const collab = ctx.collab;
|
|
3402
|
+
const startHost = collab?.startCollabHost ?? collab?.startHost;
|
|
3403
|
+
if (!startHost)
|
|
3404
|
+
throw new Error("OMP collab host facade is unavailable for Rig run process.");
|
|
3405
|
+
const identity = resolveRigIdentity(ctx);
|
|
3406
|
+
const journal = await createRunJournal(ctx.sessionManager, runId);
|
|
3407
|
+
const projectRoot = process.env.PROJECT_RIG_ROOT ?? process.cwd();
|
|
3408
|
+
let runProjection = projectRunFromSession(customEntries2(ctx.sessionManager.getBranch()), runId);
|
|
3409
|
+
const taskIdAtStart = process.env.RIG_TASK_ID?.trim() || runProjection.record.taskId;
|
|
3410
|
+
const runDisplayTitle = (() => {
|
|
3411
|
+
const base = process.env.RIG_RUN_TITLE?.trim() || `Rig run ${runId}`;
|
|
3412
|
+
return taskIdAtStart && !base.includes(taskIdAtStart) ? `[${taskIdAtStart}] ${base}` : base;
|
|
3413
|
+
})();
|
|
3414
|
+
const stopHost = collab?.stopHost?.bind(collab);
|
|
3415
|
+
let closeoutStarted = false;
|
|
3416
|
+
let forcedSteerUsed = false;
|
|
3417
|
+
let lastRunActivityAt = Date.now();
|
|
3418
|
+
const markRunActivity = () => {
|
|
3419
|
+
lastRunActivityAt = Date.now();
|
|
3420
|
+
};
|
|
3421
|
+
let runRegistryHeartbeat = null;
|
|
3422
|
+
let runRegistryRoomId = null;
|
|
3423
|
+
const runRegistryNamespace = identity?.owner?.namespaceKey ?? resolveOwnerNamespaceKey(rigProjectRoot()) ?? null;
|
|
3424
|
+
const runRegistrySecret = registrySecret();
|
|
3425
|
+
const runRegistry = runRegistryNamespace && runRegistrySecret ? createRegistryClient({ baseUrl: registryBaseUrl(), namespaceKey: runRegistryNamespace, secret: runRegistrySecret }) : null;
|
|
3426
|
+
const stopRunRegistry = () => {
|
|
3427
|
+
if (runRegistryHeartbeat) {
|
|
3428
|
+
clearInterval(runRegistryHeartbeat);
|
|
3429
|
+
runRegistryHeartbeat = null;
|
|
3430
|
+
}
|
|
3431
|
+
if (runRegistry && runRegistryRoomId) {
|
|
3432
|
+
runRegistry.removeRoom(runRegistryRoomId).catch(() => {});
|
|
3433
|
+
}
|
|
3434
|
+
};
|
|
3435
|
+
const stopController = createRunProcessStopController(journal, stopHost, undefined, {
|
|
3436
|
+
interrupt: () => ctx.abort(),
|
|
3437
|
+
isIdle: () => ctx.isIdle()
|
|
3438
|
+
});
|
|
3439
|
+
const stopHandlers = createRunProcessStopHandlers(runId, stopController);
|
|
3440
|
+
const stopRunStallMonitor = startRunProcessStallMonitor({
|
|
3441
|
+
journal,
|
|
3442
|
+
lastActivityAt: () => lastRunActivityAt,
|
|
3443
|
+
alreadyStalled: runProjection.stallCount > 0
|
|
3444
|
+
});
|
|
3445
|
+
const startRunCollabHost = async () => {
|
|
3446
|
+
try {
|
|
3447
|
+
const projection = await startHost.call(collab, {
|
|
3448
|
+
title: runDisplayTitle,
|
|
3449
|
+
...identity?.owner ? { owner: identity.owner } : {},
|
|
3450
|
+
...identity?.selectedRepo ? { selectedRepo: identity.selectedRepo } : {},
|
|
3451
|
+
relayUrl: rigRelayUrl()
|
|
3452
|
+
});
|
|
3453
|
+
const timeline = {
|
|
3454
|
+
type: "collab-host-started",
|
|
3455
|
+
roomId: projection.sessionId,
|
|
3456
|
+
joinLink: projection.joinLink,
|
|
3457
|
+
webLink: projection.webLink,
|
|
3458
|
+
relayUrl: projection.relayUrl
|
|
3459
|
+
};
|
|
3460
|
+
journal?.appendTimeline(timeline);
|
|
3461
|
+
console.log(`[rig-run] collab-host-started joinLink=${projection.joinLink || "(empty)"} relayUrl=${projection.relayUrl || "(empty)"}`);
|
|
3462
|
+
ctx.ui.notify("Rig run collab host started.", "info");
|
|
3463
|
+
if (runRegistry && identity?.owner) {
|
|
3464
|
+
try {
|
|
3465
|
+
runRegistryRoomId = projection.sessionId;
|
|
3466
|
+
await runRegistry.registerRoom({
|
|
3467
|
+
roomId: projection.sessionId,
|
|
3468
|
+
owner: identity.owner,
|
|
3469
|
+
repo: identity.selectedRepo ?? "",
|
|
3470
|
+
title: runDisplayTitle,
|
|
3471
|
+
status: "running",
|
|
3472
|
+
joinLink: projection.joinLink ?? "",
|
|
3473
|
+
webLink: projection.webLink ?? "",
|
|
3474
|
+
relayUrl: projection.relayUrl ?? rigRelayUrl(),
|
|
3475
|
+
startedAt: new Date().toISOString(),
|
|
3476
|
+
pid: process.pid
|
|
3477
|
+
});
|
|
3478
|
+
journal?.appendTimeline({ type: "registry-registered", roomId: projection.sessionId });
|
|
3479
|
+
console.log(`[rig-run] registry-registered roomId=${projection.sessionId}`);
|
|
3480
|
+
runRegistryHeartbeat = setInterval(() => {
|
|
3481
|
+
runRegistry.heartbeatRoom(projection.sessionId, "running").catch((err) => console.error(`[rig-run] registry-heartbeat-failed ${err instanceof Error ? err.message : String(err)}`));
|
|
3482
|
+
}, 15000);
|
|
3483
|
+
if (typeof runRegistryHeartbeat.unref === "function")
|
|
3484
|
+
runRegistryHeartbeat.unref();
|
|
3485
|
+
} catch (error) {
|
|
3486
|
+
console.error(`[rig-run] registry-register-failed ${error instanceof Error ? error.message : String(error)}`);
|
|
3487
|
+
ctx.ui.notify("Rig run could not register to the discovery registry; it may not appear in Runs.", "warning");
|
|
3488
|
+
}
|
|
3489
|
+
} else {
|
|
3490
|
+
console.error(`[rig-run] registry-skip namespace=${runRegistryNamespace ? "set" : "MISSING"} secret=${runRegistrySecret ? "set" : "MISSING"}`);
|
|
3491
|
+
ctx.ui.notify("Rig run NOT registered (registry namespace/secret missing) \u2014 it won't appear in Runs.", "warning");
|
|
3492
|
+
}
|
|
3493
|
+
} catch (error) {
|
|
3494
|
+
console.error(`[rig-run] collab-host-start-failed ${error instanceof Error ? error.message : String(error)}`);
|
|
3495
|
+
ctx.ui.notify("Rig run collab host could not reach the relay; session remains local.", "warning");
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
journal?.appendTimeline({ type: "stage", stage: "Connect", status: "running", detail: "run process session_start" });
|
|
3499
|
+
journal?.appendTimeline({ type: "stage", stage: "Prepare", status: "completed", detail: "run process prepared" });
|
|
3500
|
+
journal?.appendStatus("running", { actor: { kind: "agent" }, reason: "run process session started", force: true });
|
|
3501
|
+
await startRunCollabHost();
|
|
3502
|
+
if (taskIdAtStart) {
|
|
3503
|
+
journal?.appendTimeline({ type: "stage", stage: "GitHub-sync", status: "running", detail: "reflecting rig:running to task source" });
|
|
3504
|
+
try {
|
|
3505
|
+
await updateRunTaskSourceLifecycle(projectRoot, {
|
|
3506
|
+
runId,
|
|
3507
|
+
taskId: taskIdAtStart,
|
|
3508
|
+
sourceTask: runProjection.record.sourceTask,
|
|
3509
|
+
worktreePath: process.cwd(),
|
|
3510
|
+
logRoot: runProjection.record.logRoot ?? null,
|
|
3511
|
+
sessionPath: runProjection.record.sessionPath ?? null
|
|
3512
|
+
}, "running", "Rig started work on this task.");
|
|
3513
|
+
journal?.appendTimeline({ type: "stage", stage: "GitHub-sync", status: "completed", detail: "reflected running" });
|
|
3514
|
+
} catch (error) {
|
|
3515
|
+
journal?.appendTimeline({ type: "stage", stage: "GitHub-sync", status: "failed", detail: error instanceof Error ? error.message : String(error) });
|
|
3516
|
+
console.error(`[rig-run] running-reflection-failed ${error instanceof Error ? error.message : String(error)}`);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
const steerDriver = createRunProcessSteerDriver(api, journal);
|
|
3520
|
+
const operatorSteerSessionFile = ctx.sessionManager.getSessionFile();
|
|
3521
|
+
if (operatorSteerSessionFile) {
|
|
3522
|
+
startOperatorSteerConsumer({
|
|
3523
|
+
runId,
|
|
3524
|
+
projectRoot,
|
|
3525
|
+
sessionFile: operatorSteerSessionFile,
|
|
3526
|
+
requestStop: (reason) => stopController.requestStop(reason),
|
|
3527
|
+
steerPi: steerDriver.steerPi
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
api.on("before_agent_start", (event) => {
|
|
3531
|
+
markRunActivity();
|
|
3532
|
+
stopHandlers.beforeAgentStart(event);
|
|
3533
|
+
});
|
|
3534
|
+
api.on("message_start", (event) => {
|
|
3535
|
+
markRunActivity();
|
|
3536
|
+
stopHandlers.messageStart(event);
|
|
3537
|
+
});
|
|
3538
|
+
if (process.env.RIG_RUN_FORCE_STOP_ONCE === "1" || process.env.RIG_RUN_FORCE_STOP === "1")
|
|
3539
|
+
stopController.requestStop("force stop hook");
|
|
3540
|
+
api.on("agent_end", async () => {
|
|
3541
|
+
markRunActivity();
|
|
3542
|
+
if (stopController.isStopRequested()) {
|
|
3543
|
+
stopRunStallMonitor();
|
|
3544
|
+
stopRunRegistry();
|
|
3545
|
+
await stopController.finishStopped();
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
if (steerDriver.resolvePendingSteerOnAgentEnd())
|
|
3549
|
+
return;
|
|
3550
|
+
if (closeoutStarted)
|
|
3551
|
+
return;
|
|
3552
|
+
stopRunRegistry();
|
|
3553
|
+
if (process.env.RIG_RUN_FORCE_STEER_ONCE === "1" && !forcedSteerUsed) {
|
|
3554
|
+
forcedSteerUsed = true;
|
|
3555
|
+
journal?.appendTimeline({ type: "force-steer-once-started" });
|
|
3556
|
+
await steerDriver.steerPi("now reply with the word FIXED");
|
|
3557
|
+
journal?.appendTimeline({ type: "force-steer-once-completed" });
|
|
3558
|
+
}
|
|
3559
|
+
closeoutStarted = true;
|
|
3560
|
+
runProjection = projectRunFromSession(customEntries2(ctx.sessionManager.getBranch()), runId);
|
|
3561
|
+
const taskId2 = process.env.RIG_TASK_ID?.trim() || runProjection.record.taskId;
|
|
3562
|
+
if (!taskId2) {
|
|
3563
|
+
journal?.appendTimeline({ type: "closeout-skipped", reason: "missing-task-id" });
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
const { command, gitCommand } = createEnvCloseoutRunners(process.env);
|
|
3567
|
+
try {
|
|
3568
|
+
await runInProcessCloseout({
|
|
3569
|
+
projectRoot,
|
|
3570
|
+
runId,
|
|
3571
|
+
taskId: taskId2,
|
|
3572
|
+
branch: runProjection.record.branch ?? `rig/${taskId2}-${runId}`,
|
|
3573
|
+
workspace: process.cwd(),
|
|
3574
|
+
artifactRoot: runProjection.record.artifactRoot ?? null,
|
|
3575
|
+
sourceTask: runProjection.record.sourceTask && typeof runProjection.record.sourceTask === "object" && !Array.isArray(runProjection.record.sourceTask) ? runProjection.record.sourceTask : null,
|
|
3576
|
+
command,
|
|
3577
|
+
gitCommand,
|
|
3578
|
+
steerPi: steerDriver.steerPi,
|
|
3579
|
+
journalPhase: (phase, outcome, detail) => journal?.appendCloseoutPhase({ phase, outcome, detail: detail ?? null }),
|
|
3580
|
+
reflect: async (status, summary, opts) => {
|
|
3581
|
+
await updateRunTaskSourceLifecycle(projectRoot, {
|
|
3582
|
+
runId,
|
|
3583
|
+
taskId: taskId2,
|
|
3584
|
+
sourceTask: runProjection.record.sourceTask,
|
|
3585
|
+
worktreePath: process.cwd(),
|
|
3586
|
+
logRoot: runProjection.record.logRoot ?? null,
|
|
3587
|
+
sessionPath: runProjection.record.sessionPath ?? null
|
|
3588
|
+
}, status, summary, opts);
|
|
3589
|
+
}
|
|
3590
|
+
});
|
|
3591
|
+
journal?.appendStatus("completed", { actor: { kind: "agent" }, reason: "closeout completed" });
|
|
3592
|
+
await dispatchRunNotifications(projectRoot, runId, taskId2, "completed", "closeout completed");
|
|
3593
|
+
stopRunStallMonitor();
|
|
3594
|
+
process.exitCode = 0;
|
|
3595
|
+
setTimeout(() => process.exit(0), 0);
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
journal?.appendStatus("failed", { actor: { kind: "agent" }, errorText: error instanceof Error ? error.message : String(error), force: true });
|
|
3598
|
+
await dispatchRunNotifications(projectRoot, runId, taskId2, "failed", error instanceof Error ? error.message : String(error));
|
|
3599
|
+
stopRunStallMonitor();
|
|
3600
|
+
process.exitCode = 1;
|
|
3601
|
+
setTimeout(() => process.exit(1), 0);
|
|
3602
|
+
}
|
|
3603
|
+
});
|
|
3604
|
+
return true;
|
|
3605
|
+
}
|
|
3606
|
+
async function maybeStartSpikeAutohost(ctx) {
|
|
3607
|
+
if (process.env.RIG_SPIKE_AUTOHOST !== "1")
|
|
3608
|
+
return;
|
|
3609
|
+
const relayUrl = process.env.RIG_SPIKE_RELAY?.trim();
|
|
3610
|
+
if (!relayUrl)
|
|
3611
|
+
throw new Error("RIG_SPIKE_RELAY is required when RIG_SPIKE_AUTOHOST=1");
|
|
3612
|
+
const collab = ctx.collab;
|
|
3613
|
+
const startHost = collab?.startHost ?? collab?.startCollabHost;
|
|
3614
|
+
if (!startHost)
|
|
3615
|
+
throw new Error("OMP collab host facade is unavailable for detached PTY spike.");
|
|
3616
|
+
const identity = resolveRigIdentity(ctx);
|
|
3617
|
+
await startHost.call(collab, {
|
|
3618
|
+
relayUrl,
|
|
3619
|
+
title: "Rig detached PTY spike",
|
|
3620
|
+
...identity?.owner ? { owner: identity.owner } : {},
|
|
3621
|
+
...identity?.selectedRepo ? { selectedRepo: identity.selectedRepo } : {}
|
|
3622
|
+
});
|
|
3623
|
+
}
|
|
3624
|
+
async function startCockpitOrSetup(api, ctx, injectedBridge) {
|
|
3625
|
+
const actions = defaultFlowActions(api, ctx, injectedBridge);
|
|
3626
|
+
const status = await detectRigStartupStatus(ctx);
|
|
3627
|
+
if (status.configured) {
|
|
3628
|
+
launchRigFlow(api, ctx, "runs", actions, true, injectedBridge, true);
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
if (!hasInteractiveSetupTerminal(ctx)) {
|
|
3632
|
+
ctx.ui.notify(`Rig Setup needed: ${status.reasons.join("; ")}`, "warning");
|
|
3633
|
+
launchRigFlow(api, ctx, "setup", actions, true, injectedBridge, true);
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
try {
|
|
3637
|
+
await runRigSetupFlow(api, ctx, injectedBridge);
|
|
3638
|
+
launchRigFlow(api, ctx, "runs", actions, true, injectedBridge, true);
|
|
3639
|
+
} catch (error) {
|
|
3640
|
+
ctx.ui.notify(notificationMessage(error), "error");
|
|
3641
|
+
launchRigFlow(api, ctx, "setup", actions, true, injectedBridge, true);
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
var __rigExtensionTest = {
|
|
3645
|
+
itemsFor,
|
|
3646
|
+
defaultFlowActions,
|
|
3647
|
+
commandFlowActions,
|
|
3648
|
+
loadRigFlowState,
|
|
3649
|
+
createRunProcessSteerDriver,
|
|
3650
|
+
createRunProcessStopController,
|
|
3651
|
+
createRunProcessStopHandlers,
|
|
3652
|
+
detectRigStartupStatus,
|
|
3653
|
+
applyRigSetupProject,
|
|
3654
|
+
ensureGitHubLabels,
|
|
3655
|
+
ensurePiRigInstalledForSetup,
|
|
3656
|
+
parseRepoSlugFromRemote,
|
|
3657
|
+
stopSentinel,
|
|
3658
|
+
parseStopSentinel,
|
|
3659
|
+
applyOperatorSteers,
|
|
3660
|
+
readOperatorSteerTexts,
|
|
3661
|
+
computeRunStall,
|
|
3662
|
+
appendRunStallDetected,
|
|
3663
|
+
buildStopSentinel
|
|
3664
|
+
};
|
|
3665
|
+
function rigExtension(api, options = {}) {
|
|
3666
|
+
const injectedBridge = options.selectedServerBridge ?? null;
|
|
3667
|
+
api.registerMessageRenderer(RIG_WORKFLOW_STARTED, () => {
|
|
3668
|
+
return;
|
|
3669
|
+
});
|
|
3670
|
+
api.registerMessageRenderer(RIG_WORKFLOW_TARGET_SELECTED, () => {
|
|
3671
|
+
return;
|
|
3672
|
+
});
|
|
3673
|
+
api.registerMessageRenderer(RIG_WORKFLOW_TASK_SELECTED, () => {
|
|
3674
|
+
return;
|
|
3675
|
+
});
|
|
3676
|
+
api.registerMessageRenderer(RIG_WORKFLOW_STATUS_CHANGED, () => {
|
|
3677
|
+
return;
|
|
3678
|
+
});
|
|
3679
|
+
api.registerMessageRenderer(RIG_WORKFLOW_OPERATOR_NOTE, () => {
|
|
3680
|
+
return;
|
|
3681
|
+
});
|
|
3682
|
+
api.registerMessageRenderer(RIG_WORKFLOW_INBOX_REQUESTED, () => {
|
|
3683
|
+
return;
|
|
3684
|
+
});
|
|
3685
|
+
api.registerMessageRenderer(RIG_WORKFLOW_INBOX_RESOLVED, () => {
|
|
3686
|
+
return;
|
|
3687
|
+
});
|
|
3688
|
+
registerWorkflowInboxTool(api);
|
|
3689
|
+
api.on("session_start", async (_event, ctx) => {
|
|
3690
|
+
const isRunProcess = await maybeStartRunProcessAutohost(api, ctx);
|
|
3691
|
+
if (isRunProcess)
|
|
3692
|
+
return;
|
|
3693
|
+
if (!ctx.hasUI)
|
|
3694
|
+
return;
|
|
3695
|
+
ctx.ui.setTitle("RIG");
|
|
3696
|
+
await maybeStartSpikeAutohost(ctx);
|
|
3697
|
+
await startCockpitOrSetup(api, ctx, injectedBridge);
|
|
3698
|
+
});
|
|
3699
|
+
api.registerShortcut("g", {
|
|
3700
|
+
description: "Rig cockpit",
|
|
3701
|
+
handler: async (ctx) => {
|
|
3702
|
+
launchRigFlow(api, ctx, "cockpit", defaultFlowActions(api, ctx, injectedBridge), true, injectedBridge);
|
|
3703
|
+
}
|
|
3704
|
+
});
|
|
3705
|
+
api.registerShortcut("r", {
|
|
3706
|
+
description: "Rig runs",
|
|
3707
|
+
handler: async (ctx) => {
|
|
3708
|
+
launchRigFlow(api, ctx, "runs", defaultFlowActions(api, ctx, injectedBridge), true, injectedBridge);
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
api.registerShortcut("s", {
|
|
3712
|
+
description: "Rig server target",
|
|
3713
|
+
handler: async (ctx) => {
|
|
3714
|
+
launchRigFlow(api, ctx, "server", defaultFlowActions(api, ctx, injectedBridge), true, injectedBridge);
|
|
3715
|
+
}
|
|
3716
|
+
});
|
|
3717
|
+
api.registerShortcut("i", {
|
|
3718
|
+
description: "Rig inbox",
|
|
3719
|
+
handler: async (ctx) => {
|
|
3720
|
+
launchRigFlow(api, ctx, "inbox", defaultFlowActions(api, ctx, injectedBridge), true, injectedBridge);
|
|
3721
|
+
}
|
|
3722
|
+
});
|
|
3723
|
+
api.registerShortcut("d", {
|
|
3724
|
+
description: "Rig doctor",
|
|
3725
|
+
handler: async (ctx) => {
|
|
3726
|
+
launchRigFlow(api, ctx, "doctor", defaultFlowActions(api, ctx, injectedBridge), true, injectedBridge);
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
api.registerShortcut("n", {
|
|
3730
|
+
description: "Rig new run (tasks)",
|
|
3731
|
+
handler: async (ctx) => {
|
|
3732
|
+
launchRigFlow(api, ctx, "tasks", defaultFlowActions(api, ctx, injectedBridge), true, injectedBridge);
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
api.registerCommand("rig", {
|
|
3736
|
+
description: "Open the Rig operator flow",
|
|
3737
|
+
handler: async (args, ctx) => {
|
|
3738
|
+
const requested = args.trim();
|
|
3739
|
+
const actions = commandFlowActions(api, ctx, injectedBridge);
|
|
3740
|
+
if (requested.startsWith("action ")) {
|
|
3741
|
+
await actions.act(requested.slice("action ".length).trim());
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
if (requested.startsWith("run ")) {
|
|
3745
|
+
await actions.act(`run:${requested.slice("run ".length).trim()}`);
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
const lower = requested.toLowerCase();
|
|
3749
|
+
const screen = isRigScreen(lower) ? lower : "cockpit";
|
|
3750
|
+
launchRigFlow(api, ctx, screen, actions, true, injectedBridge);
|
|
3751
|
+
}
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
export {
|
|
3755
|
+
rigExtension as default,
|
|
3756
|
+
__rigExtensionTest
|
|
3757
|
+
};
|