@h-rig/server 0.0.6-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/src/bootstrap.js +161 -0
- package/dist/src/index.js +13153 -0
- package/dist/src/inspector/agent-runtime.js +1077 -0
- package/dist/src/inspector/analysis.js +41 -0
- package/dist/src/inspector/discovery.js +137 -0
- package/dist/src/inspector/journal.js +518 -0
- package/dist/src/inspector/mission.js +562 -0
- package/dist/src/inspector/prompt.js +97 -0
- package/dist/src/inspector/provider-session.js +65 -0
- package/dist/src/inspector/reconcile.js +118 -0
- package/dist/src/inspector/review.js +13 -0
- package/dist/src/inspector/service.js +1759 -0
- package/dist/src/inspector/skills.js +155 -0
- package/dist/src/inspector/tools.js +1592 -0
- package/dist/src/inspector/types.js +1 -0
- package/dist/src/inspector/upstream-sync.js +479 -0
- package/dist/src/orchestration.js +402 -0
- package/dist/src/remote.js +123 -0
- package/dist/src/scheduler.js +84 -0
- package/dist/src/server-helpers/broadcasters.js +161 -0
- package/dist/src/server-helpers/conversation-snapshot.js +382 -0
- package/dist/src/server-helpers/event-emitter.js +41 -0
- package/dist/src/server-helpers/github-auth-store.js +155 -0
- package/dist/src/server-helpers/github-credentials.js +38 -0
- package/dist/src/server-helpers/github-project-status-sync.js +196 -0
- package/dist/src/server-helpers/github-projects.js +147 -0
- package/dist/src/server-helpers/github-reconciler.js +89 -0
- package/dist/src/server-helpers/http-router.js +3781 -0
- package/dist/src/server-helpers/http-utils.js +135 -0
- package/dist/src/server-helpers/inspector-agent-lifecycle.js +104 -0
- package/dist/src/server-helpers/inspector-jobs.js +4145 -0
- package/dist/src/server-helpers/issue-analysis.js +362 -0
- package/dist/src/server-helpers/normalizers.js +31 -0
- package/dist/src/server-helpers/notifications.js +96 -0
- package/dist/src/server-helpers/orchestration-ops.js +287 -0
- package/dist/src/server-helpers/orchestration.js +39 -0
- package/dist/src/server-helpers/plugin-host-cache.js +86 -0
- package/dist/src/server-helpers/project-fs-ops.js +194 -0
- package/dist/src/server-helpers/project-registry.js +124 -0
- package/dist/src/server-helpers/queue-state.js +78 -0
- package/dist/src/server-helpers/remote-checkout.js +140 -0
- package/dist/src/server-helpers/remote-snapshots.js +119 -0
- package/dist/src/server-helpers/run-io.js +262 -0
- package/dist/src/server-helpers/run-mutations.js +1784 -0
- package/dist/src/server-helpers/run-steering.js +176 -0
- package/dist/src/server-helpers/run-writers.js +75 -0
- package/dist/src/server-helpers/server-paths.js +27 -0
- package/dist/src/server-helpers/snapshot-orchestrator.js +832 -0
- package/dist/src/server-helpers/snapshot-service.js +1143 -0
- package/dist/src/server-helpers/summaries.js +126 -0
- package/dist/src/server-helpers/task-config.js +50 -0
- package/dist/src/server-helpers/task-projection.js +98 -0
- package/dist/src/server-helpers/terminal-runtime.js +156 -0
- package/dist/src/server-helpers/terminal-sessions.js +22 -0
- package/dist/src/server-helpers/validation-failure.js +31 -0
- package/dist/src/server-helpers/ws-router.js +1308 -0
- package/dist/src/server.js +12628 -0
- package/dist/src/websocket.js +63 -0
- package/package.json +33 -0
|
@@ -0,0 +1,1784 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/server/src/server-helpers/run-mutations.ts
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { loadConfig } from "@rig/core/load-config";
|
|
5
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
6
|
+
import { dirname as dirname5, relative as relative2, resolve as resolve9 } from "path";
|
|
7
|
+
import {
|
|
8
|
+
listAuthorityRuns as listAuthorityRuns7,
|
|
9
|
+
readAuthorityRun as readAuthorityRun8,
|
|
10
|
+
resolveAuthorityRunDir as resolveAuthorityRunDir4,
|
|
11
|
+
writeJsonFile as writeJsonFile4
|
|
12
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
13
|
+
import { readPublishedRigServerStateSync } from "@rig/runtime/local-server";
|
|
14
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot6 } from "@rig/runtime/control-plane/native/utils";
|
|
15
|
+
import {
|
|
16
|
+
buildTaskRunLifecycleComment as buildTaskRunLifecycleComment2,
|
|
17
|
+
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
18
|
+
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
19
|
+
|
|
20
|
+
// packages/server/src/scheduler.ts
|
|
21
|
+
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
22
|
+
var TERMINAL_RUN_STATUSES = new Set(["done", "completed", "error", "failed", "stopped", "cancelled"]);
|
|
23
|
+
var RUNNABLE_TASK_STATUSES = new Set(["draft", "open", "ready", "queued"]);
|
|
24
|
+
var REMOTE_READY_STATUSES = new Set(["ready", "idle", "connected"]);
|
|
25
|
+
function resolveLocalSchedulerWorkerCount(env = process.env) {
|
|
26
|
+
const raw = env.RIG_SCHEDULER_LOCAL_WORKERS?.trim();
|
|
27
|
+
if (!raw) {
|
|
28
|
+
return Number.MAX_SAFE_INTEGER;
|
|
29
|
+
}
|
|
30
|
+
const parsed = Number.parseInt(raw, 10);
|
|
31
|
+
if (!Number.isFinite(parsed)) {
|
|
32
|
+
return Number.MAX_SAFE_INTEGER;
|
|
33
|
+
}
|
|
34
|
+
return Math.max(0, parsed);
|
|
35
|
+
}
|
|
36
|
+
function planSchedulerWork(input) {
|
|
37
|
+
const taskById = new Map(input.tasks.map((task) => [task.id, task]));
|
|
38
|
+
const activeTaskIds = new Set(input.runs.filter((run) => isActiveRun(run) && typeof run.taskId === "string" && run.taskId.length > 0).map((run) => run.taskId));
|
|
39
|
+
const activeLocalRuns = input.runs.filter((run) => isActiveRun(run) && typeof run.taskId === "string" && run.taskId.length > 0 && !isRemoteRun(run));
|
|
40
|
+
const occupiedRemoteHosts = new Set(input.runs.filter((run) => isActiveRun(run) && isRemoteRun(run) && typeof run.hostId === "string" && run.hostId.length > 0).map((run) => run.hostId));
|
|
41
|
+
const pruneTaskIds = [];
|
|
42
|
+
const eligibleQueue = input.queue.filter((entry) => {
|
|
43
|
+
const task = taskById.get(entry.taskId);
|
|
44
|
+
const normalizedStatus = task ? normalizeTaskLifecycleStatus(task.status) : null;
|
|
45
|
+
if (!task || !normalizedStatus || !RUNNABLE_TASK_STATUSES.has(normalizedStatus)) {
|
|
46
|
+
pruneTaskIds.push(entry.taskId);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (activeTaskIds.has(entry.taskId)) {
|
|
50
|
+
pruneTaskIds.push(entry.taskId);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
let freeLocalWorkers = Math.max(0, input.localWorkerCount - activeLocalRuns.length);
|
|
56
|
+
const dispatches = [];
|
|
57
|
+
const deferredForRemote = [];
|
|
58
|
+
for (const entry of eligibleQueue) {
|
|
59
|
+
if (freeLocalWorkers > 0) {
|
|
60
|
+
dispatches.push({ taskId: entry.taskId, workerKind: "local" });
|
|
61
|
+
freeLocalWorkers -= 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
deferredForRemote.push(entry);
|
|
65
|
+
}
|
|
66
|
+
const availableRemoteWorkers = input.remoteWorkers.filter((worker) => REMOTE_READY_STATUSES.has(worker.status)).filter((worker) => !occupiedRemoteHosts.has(worker.hostId)).toSorted((left, right) => {
|
|
67
|
+
if (left.currentLeaseCount !== right.currentLeaseCount) {
|
|
68
|
+
return left.currentLeaseCount - right.currentLeaseCount;
|
|
69
|
+
}
|
|
70
|
+
return left.registeredAt.localeCompare(right.registeredAt) || left.hostId.localeCompare(right.hostId);
|
|
71
|
+
});
|
|
72
|
+
for (let index = 0;index < Math.min(deferredForRemote.length, availableRemoteWorkers.length); index += 1) {
|
|
73
|
+
const entry = deferredForRemote[index];
|
|
74
|
+
const worker = availableRemoteWorkers[index];
|
|
75
|
+
if (!entry || !worker) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
dispatches.push({
|
|
79
|
+
taskId: entry.taskId,
|
|
80
|
+
workerKind: "remote",
|
|
81
|
+
hostId: worker.hostId
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
pruneTaskIds: Array.from(new Set(pruneTaskIds)),
|
|
86
|
+
dispatches
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function isActiveRun(run) {
|
|
90
|
+
const status = normalizeStatus(run.status);
|
|
91
|
+
return !TERMINAL_RUN_STATUSES.has(status);
|
|
92
|
+
}
|
|
93
|
+
function isRemoteRun(run) {
|
|
94
|
+
return normalizeStatus(run.mode) === "remote";
|
|
95
|
+
}
|
|
96
|
+
function normalizeStatus(value) {
|
|
97
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// packages/server/src/server.ts
|
|
101
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
102
|
+
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
103
|
+
import {
|
|
104
|
+
listAuthorityArtifactRoots,
|
|
105
|
+
listAuthorityRuns as listAuthorityRuns6,
|
|
106
|
+
readJsonFile as readJsonFile3,
|
|
107
|
+
readJsonlFile as readJsonlFile3
|
|
108
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
109
|
+
import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus3 } from "@rig/runtime/control-plane/state-sync/types";
|
|
110
|
+
import {
|
|
111
|
+
readWorkspaceSummary
|
|
112
|
+
} from "@rig/runtime/control-plane/native/workspace-ops";
|
|
113
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot5 } from "@rig/runtime/control-plane/native/utils";
|
|
114
|
+
|
|
115
|
+
// packages/server/src/bootstrap.ts
|
|
116
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
117
|
+
import { dirname, resolve } from "path";
|
|
118
|
+
import { RIG_DEFINITION_DIRNAME, resolveMonorepoRoot } from "@rig/runtime";
|
|
119
|
+
function normalizeOptionalString(value) {
|
|
120
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
121
|
+
}
|
|
122
|
+
function resolveRigServerPaths(projectRoot) {
|
|
123
|
+
const taskWorkspace = normalizeOptionalString(process.env.RIG_TASK_WORKSPACE);
|
|
124
|
+
const explicitStateDir = normalizeOptionalString(process.env.RIG_STATE_DIR);
|
|
125
|
+
const explicitLogsDir = normalizeOptionalString(process.env.RIG_LOGS_DIR);
|
|
126
|
+
const explicitSessionFile = normalizeOptionalString(process.env.RIG_SESSION_FILE);
|
|
127
|
+
const hostStateRoot = resolve(projectRoot, ".rig");
|
|
128
|
+
const monorepoRoot = resolveMonorepoRoot(projectRoot);
|
|
129
|
+
const monorepoStateRoot = resolve(monorepoRoot, ".rig");
|
|
130
|
+
const stateRoot = taskWorkspace ? resolve(taskWorkspace, ".rig") : explicitStateDir ? dirname(resolve(explicitStateDir)) : explicitLogsDir ? dirname(resolve(explicitLogsDir)) : explicitSessionFile ? dirname(dirname(resolve(explicitSessionFile))) : existsSync(hostStateRoot) ? hostStateRoot : monorepoStateRoot;
|
|
131
|
+
const stateDir = explicitStateDir ? resolve(explicitStateDir) : resolve(stateRoot, "state");
|
|
132
|
+
const logsDir = explicitLogsDir ? resolve(explicitLogsDir) : resolve(stateRoot, "logs");
|
|
133
|
+
const taskConfigPath = taskWorkspace ? resolve(taskWorkspace, ".rig", "task-config.json") : existsSync(resolve(projectRoot, ".rig", "task-config.json")) ? resolve(projectRoot, ".rig", "task-config.json") : resolve(monorepoStateRoot, "task-config.json");
|
|
134
|
+
return {
|
|
135
|
+
stateRoot,
|
|
136
|
+
stateDir,
|
|
137
|
+
logsDir,
|
|
138
|
+
controlPlaneEventsFile: resolve(logsDir, "control-plane.events.jsonl"),
|
|
139
|
+
taskConfigPath,
|
|
140
|
+
notificationsFile: resolve(projectRoot, "rig", "notifications", "targets.json"),
|
|
141
|
+
keybindingsPath: resolve(projectRoot, "rig", "keybindings.json")
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// packages/server/src/websocket.ts
|
|
146
|
+
import {
|
|
147
|
+
WS_CHANNELS
|
|
148
|
+
} from "@rig/contracts";
|
|
149
|
+
function encodeWebSocketPayload(payload) {
|
|
150
|
+
return JSON.stringify(payload);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// packages/server/src/server-helpers/normalizers.ts
|
|
154
|
+
function normalizeString(value) {
|
|
155
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
156
|
+
}
|
|
157
|
+
function normalizeStringArray(value) {
|
|
158
|
+
return Array.isArray(value) ? value.map((entry) => normalizeString(entry)).filter((entry) => entry !== null) : [];
|
|
159
|
+
}
|
|
160
|
+
function normalizeRuntimeAdapter(value) {
|
|
161
|
+
const normalized = normalizeString(value)?.toLowerCase();
|
|
162
|
+
if (!normalized) {
|
|
163
|
+
return "pi";
|
|
164
|
+
}
|
|
165
|
+
if (normalized === "codex" || normalized === "codex-cli" || normalized === "codex-app-server" || normalized === "gpt-codex") {
|
|
166
|
+
return "codex";
|
|
167
|
+
}
|
|
168
|
+
if (normalized === "pi" || normalized === "rig-pi" || normalized === "@rig/pi") {
|
|
169
|
+
return "pi";
|
|
170
|
+
}
|
|
171
|
+
return "claude-code";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// packages/server/src/server-helpers/run-io.ts
|
|
175
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
176
|
+
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
177
|
+
import {
|
|
178
|
+
listAuthorityRuns,
|
|
179
|
+
readAuthorityRun,
|
|
180
|
+
readJsonlFile,
|
|
181
|
+
resolveAuthorityRunDir
|
|
182
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
183
|
+
function runLogsPath(projectRoot, runId) {
|
|
184
|
+
return resolve2(resolveAuthorityRunDir(projectRoot, runId), "logs.jsonl");
|
|
185
|
+
}
|
|
186
|
+
function parseJsonlRecords(lines) {
|
|
187
|
+
return lines.map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
188
|
+
try {
|
|
189
|
+
return [JSON.parse(line)];
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function readJsonlFileTail(path, options) {
|
|
196
|
+
const limit = Math.max(0, Math.trunc(options.limit));
|
|
197
|
+
if (limit === 0 || !existsSync2(path))
|
|
198
|
+
return [];
|
|
199
|
+
const maxBytes = Math.max(1024, Math.trunc(options.maxBytes ?? 256 * 1024));
|
|
200
|
+
const size = statSync(path).size;
|
|
201
|
+
if (size === 0)
|
|
202
|
+
return [];
|
|
203
|
+
const start = Math.max(0, size - maxBytes);
|
|
204
|
+
const length = size - start;
|
|
205
|
+
const buffer = Buffer.alloc(length);
|
|
206
|
+
const fd = openSync(path, "r");
|
|
207
|
+
try {
|
|
208
|
+
readSync(fd, buffer, 0, length, start);
|
|
209
|
+
} finally {
|
|
210
|
+
closeSync(fd);
|
|
211
|
+
}
|
|
212
|
+
const text = buffer.toString("utf8");
|
|
213
|
+
const lines = text.split(/\r?\n/);
|
|
214
|
+
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
215
|
+
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
216
|
+
}
|
|
217
|
+
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
218
|
+
function readLatestRawRunLog(projectRoot, runId) {
|
|
219
|
+
const logs = readJsonlFileTail(runLogsPath(projectRoot, runId), { limit: 20 });
|
|
220
|
+
return logs.findLast((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry))) ?? null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// packages/server/src/server-helpers/server-paths.ts
|
|
224
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
225
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot2 } from "@rig/runtime/control-plane/native/utils";
|
|
226
|
+
function resolveServerAuthorityPaths(projectRoot) {
|
|
227
|
+
const taskWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
228
|
+
const explicitStateDir = process.env.RIG_STATE_DIR?.trim();
|
|
229
|
+
const explicitLogsDir = process.env.RIG_LOGS_DIR?.trim();
|
|
230
|
+
const explicitSessionFile = process.env.RIG_SESSION_FILE?.trim();
|
|
231
|
+
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
232
|
+
const stateRoot = taskWorkspace ? resolve3(taskWorkspace, ".rig") : explicitStateDir ? dirname3(resolve3(explicitStateDir)) : explicitLogsDir ? dirname3(resolve3(explicitLogsDir)) : explicitSessionFile ? dirname3(dirname3(resolve3(explicitSessionFile))) : resolve3(monorepoRoot, ".rig");
|
|
233
|
+
const stateDir = explicitStateDir ? resolve3(explicitStateDir) : resolve3(stateRoot, "state");
|
|
234
|
+
const logsDir = explicitLogsDir ? resolve3(explicitLogsDir) : resolve3(stateRoot, "logs");
|
|
235
|
+
const sessionFile = explicitSessionFile ? resolve3(explicitSessionFile) : resolve3(stateRoot, "session", "session.json");
|
|
236
|
+
const artifactsDir = taskWorkspace ? resolve3(taskWorkspace, "artifacts") : resolve3(monorepoRoot, "artifacts");
|
|
237
|
+
return {
|
|
238
|
+
stateRoot,
|
|
239
|
+
stateDir,
|
|
240
|
+
logsDir,
|
|
241
|
+
controlPlaneEventsFile: resolve3(logsDir, "control-plane.events.jsonl"),
|
|
242
|
+
sessionFile,
|
|
243
|
+
artifactsDir
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// packages/server/src/server-helpers/snapshot-service.ts
|
|
248
|
+
import { listAgentRuntimes } from "@rig/runtime/control-plane/runtime/isolation";
|
|
249
|
+
|
|
250
|
+
// packages/server/src/server-helpers/snapshot-orchestrator.ts
|
|
251
|
+
import {
|
|
252
|
+
listAuthorityRuns as listAuthorityRuns3,
|
|
253
|
+
readAuthorityRun as readAuthorityRun2
|
|
254
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
255
|
+
|
|
256
|
+
// packages/server/src/server-helpers/queue-state.ts
|
|
257
|
+
import { resolve as resolve4 } from "path";
|
|
258
|
+
import { readJsonFile, writeJsonFile } from "@rig/runtime/control-plane/authority-files";
|
|
259
|
+
function resolveQueueStatePath(projectRoot) {
|
|
260
|
+
return resolve4(resolveServerAuthorityPaths(projectRoot).stateDir, "task-queue.json");
|
|
261
|
+
}
|
|
262
|
+
function readQueueState(projectRoot) {
|
|
263
|
+
const queue = readJsonFile(resolveQueueStatePath(projectRoot), null);
|
|
264
|
+
if (!Array.isArray(queue))
|
|
265
|
+
return [];
|
|
266
|
+
return queue.filter((entry) => entry && typeof entry === "object").map((entry, index) => ({
|
|
267
|
+
taskId: normalizeString(entry.taskId),
|
|
268
|
+
score: typeof entry.score === "number" ? Math.max(0, Math.trunc(entry.score)) : 0,
|
|
269
|
+
unblockCount: typeof entry.unblockCount === "number" ? Math.max(0, Math.trunc(entry.unblockCount)) : 0,
|
|
270
|
+
position: typeof entry.position === "number" ? Math.max(0, Math.trunc(entry.position)) : index
|
|
271
|
+
})).filter((entry) => Boolean(entry.taskId)).sort((left, right) => right.score - left.score || left.position - right.position).map((entry, index) => ({ ...entry, position: index }));
|
|
272
|
+
}
|
|
273
|
+
function writeQueueState(projectRoot, queue) {
|
|
274
|
+
writeJsonFile(resolveQueueStatePath(projectRoot), queue);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// packages/server/src/server-helpers/remote-snapshots.ts
|
|
278
|
+
import { listAuthorityRemoteEndpoints } from "@rig/runtime/control-plane/authority-files";
|
|
279
|
+
function normalizeIsoTimestamp(value) {
|
|
280
|
+
const normalized = normalizeString(value);
|
|
281
|
+
if (!normalized)
|
|
282
|
+
return null;
|
|
283
|
+
const timestamp = Date.parse(normalized);
|
|
284
|
+
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// packages/server/src/server-helpers/conversation-snapshot.ts
|
|
288
|
+
import {
|
|
289
|
+
listAuthorityRuns as listAuthorityRuns2,
|
|
290
|
+
readJsonlFile as readJsonlFile2
|
|
291
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
292
|
+
var snapshotCache = new Map;
|
|
293
|
+
|
|
294
|
+
// packages/server/src/server-helpers/plugin-host-cache.ts
|
|
295
|
+
var contextCache = new Map;
|
|
296
|
+
var taskListCache = new Map;
|
|
297
|
+
|
|
298
|
+
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
299
|
+
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
300
|
+
|
|
301
|
+
// packages/server/src/server-helpers/broadcasters.ts
|
|
302
|
+
import { RIG_WS_CHANNELS } from "@rig/contracts";
|
|
303
|
+
|
|
304
|
+
// packages/server/src/server-helpers/run-writers.ts
|
|
305
|
+
import { resolve as resolve5 } from "path";
|
|
306
|
+
import {
|
|
307
|
+
appendJsonlRecord,
|
|
308
|
+
readAuthorityRun as readAuthorityRun3,
|
|
309
|
+
resolveAuthorityRunDir as resolveAuthorityRunDir2,
|
|
310
|
+
writeJsonFile as writeJsonFile2
|
|
311
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
312
|
+
function appendRunLogEntry(projectRoot, runId, value) {
|
|
313
|
+
if (!readAuthorityRun3(projectRoot, runId)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
appendJsonlRecord(runLogsPath(projectRoot, runId), value);
|
|
317
|
+
patchRunRecord(projectRoot, runId, {});
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
function patchRunRecord(projectRoot, runId, patch) {
|
|
321
|
+
const current = readAuthorityRun3(projectRoot, runId);
|
|
322
|
+
if (!current) {
|
|
323
|
+
throw new Error(`Run not found: ${runId}`);
|
|
324
|
+
}
|
|
325
|
+
const next = {
|
|
326
|
+
...current,
|
|
327
|
+
...patch,
|
|
328
|
+
updatedAt: normalizeString(patch.updatedAt) ?? new Date().toISOString()
|
|
329
|
+
};
|
|
330
|
+
writeJsonFile2(resolve5(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
|
|
331
|
+
return next;
|
|
332
|
+
}
|
|
333
|
+
function buildRunStartPatch(startedAt) {
|
|
334
|
+
return {
|
|
335
|
+
status: "preparing",
|
|
336
|
+
startedAt,
|
|
337
|
+
completedAt: null,
|
|
338
|
+
errorText: null,
|
|
339
|
+
serverPid: process.pid
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// packages/server/src/server-helpers/event-emitter.ts
|
|
344
|
+
function nextSequence(carrier) {
|
|
345
|
+
const current = carrier.sequence ?? 0;
|
|
346
|
+
const next = current + 1;
|
|
347
|
+
carrier.sequence = next;
|
|
348
|
+
return next;
|
|
349
|
+
}
|
|
350
|
+
function buildRigEvent(carrier, input) {
|
|
351
|
+
const createdAt = normalizeIsoTimestamp(input.createdAt) ?? new Date().toISOString();
|
|
352
|
+
const sequence = nextSequence(carrier);
|
|
353
|
+
return {
|
|
354
|
+
id: `rig-event-${sequence}`,
|
|
355
|
+
sequence,
|
|
356
|
+
createdAt,
|
|
357
|
+
type: input.type,
|
|
358
|
+
aggregateId: input.aggregateId,
|
|
359
|
+
payload: input.payload ?? {}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// packages/server/src/server-helpers/broadcasters.ts
|
|
364
|
+
var SERVER_EVENT_JOURNAL_LIMIT = 5000;
|
|
365
|
+
function broadcastPush(state, payload) {
|
|
366
|
+
const encoded = encodeWebSocketPayload(payload);
|
|
367
|
+
for (const socket of state.sockets) {
|
|
368
|
+
try {
|
|
369
|
+
socket.send(encoded);
|
|
370
|
+
} catch {
|
|
371
|
+
state.sockets.delete(socket);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function buildSnapshotInvalidatedPush(state, reason = "snapshot-mutated") {
|
|
376
|
+
return {
|
|
377
|
+
type: "push",
|
|
378
|
+
channel: RIG_WS_CHANNELS.snapshotInvalidated,
|
|
379
|
+
data: {
|
|
380
|
+
sequence: state.sequence,
|
|
381
|
+
updatedAt: new Date().toISOString(),
|
|
382
|
+
reason
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function broadcastSnapshotInvalidation(state, reason) {
|
|
387
|
+
broadcastPush(state, buildSnapshotInvalidatedPush(state, reason));
|
|
388
|
+
}
|
|
389
|
+
function buildRunLogAppendedPush(runId, entry) {
|
|
390
|
+
return {
|
|
391
|
+
type: "push",
|
|
392
|
+
channel: RIG_WS_CHANNELS.runLogAppended,
|
|
393
|
+
data: {
|
|
394
|
+
runId,
|
|
395
|
+
entry
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function broadcastRunLogAppended(state, runId, entry) {
|
|
400
|
+
if (!entry) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
broadcastPush(state, buildRunLogAppendedPush(runId, entry));
|
|
404
|
+
}
|
|
405
|
+
function appendRunLogEntryAndBroadcast(state, runId, value, reason) {
|
|
406
|
+
const entry = appendRunLogEntry(state.projectRoot, runId, value);
|
|
407
|
+
broadcastRunLogAppended(state, runId, entry);
|
|
408
|
+
broadcastSnapshotInvalidation(state, reason);
|
|
409
|
+
return entry;
|
|
410
|
+
}
|
|
411
|
+
function emitRigEvent(state, input) {
|
|
412
|
+
const event = buildRigEvent(state, input);
|
|
413
|
+
state.eventJournal.push(event);
|
|
414
|
+
if (state.eventJournal.length > SERVER_EVENT_JOURNAL_LIMIT) {
|
|
415
|
+
state.eventJournal.splice(0, state.eventJournal.length - SERVER_EVENT_JOURNAL_LIMIT);
|
|
416
|
+
}
|
|
417
|
+
broadcastPush(state, {
|
|
418
|
+
type: "push",
|
|
419
|
+
channel: RIG_WS_CHANNELS.event,
|
|
420
|
+
data: event
|
|
421
|
+
});
|
|
422
|
+
return event;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
426
|
+
var DEFAULT_TERMINAL_SHELL = process.env.SHELL || "/bin/zsh";
|
|
427
|
+
|
|
428
|
+
// packages/server/src/server-helpers/http-router.ts
|
|
429
|
+
import {
|
|
430
|
+
listAuthorityRuns as listAuthorityRuns4,
|
|
431
|
+
readAuthorityRun as readAuthorityRun5,
|
|
432
|
+
resolveAuthorityPaths,
|
|
433
|
+
writeJsonFile as writeJsonFile3
|
|
434
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
435
|
+
import {
|
|
436
|
+
mutateWorkspaceServiceFabric,
|
|
437
|
+
readTaskArtifactPreview,
|
|
438
|
+
readWorkspaceRemoteFleet,
|
|
439
|
+
readWorkspaceTopology
|
|
440
|
+
} from "@rig/runtime/control-plane/native/workspace-ops";
|
|
441
|
+
import {
|
|
442
|
+
doctorManagedRemoteEndpoints,
|
|
443
|
+
listManagedRemoteEndpoints,
|
|
444
|
+
removeManagedRemoteEndpoint,
|
|
445
|
+
resolveRemoteEndpoint,
|
|
446
|
+
updateManagedRemoteEndpointInAuthority,
|
|
447
|
+
upsertManagedRemoteEndpoint,
|
|
448
|
+
RemoteWsClient
|
|
449
|
+
} from "@rig/runtime/control-plane/remote";
|
|
450
|
+
|
|
451
|
+
// packages/server/src/server-helpers/run-steering.ts
|
|
452
|
+
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun4, resolveAuthorityRunDir as resolveAuthorityRunDir3 } from "@rig/runtime/control-plane/authority-files";
|
|
453
|
+
|
|
454
|
+
// packages/server/src/server-helpers/http-router.ts
|
|
455
|
+
import { buildRigInitConfigSource } from "@rig/core";
|
|
456
|
+
import {
|
|
457
|
+
buildTaskRunLifecycleComment,
|
|
458
|
+
updateConfiguredTaskSourceTask
|
|
459
|
+
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
460
|
+
|
|
461
|
+
// packages/server/src/server-helpers/github-auth-store.ts
|
|
462
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
463
|
+
import { resolve as resolve6 } from "path";
|
|
464
|
+
function cleanString(value) {
|
|
465
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
466
|
+
}
|
|
467
|
+
function cleanScopes(value) {
|
|
468
|
+
if (!Array.isArray(value))
|
|
469
|
+
return [];
|
|
470
|
+
return value.flatMap((entry) => {
|
|
471
|
+
const clean = cleanString(entry);
|
|
472
|
+
return clean ? [clean] : [];
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
function readStoredAuth(stateFile) {
|
|
476
|
+
if (!existsSync3(stateFile))
|
|
477
|
+
return {};
|
|
478
|
+
try {
|
|
479
|
+
const parsed = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
480
|
+
return {
|
|
481
|
+
...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
|
|
482
|
+
login: cleanString(parsed.login),
|
|
483
|
+
userId: cleanString(parsed.userId),
|
|
484
|
+
scopes: cleanScopes(parsed.scopes),
|
|
485
|
+
selectedRepo: cleanString(parsed.selectedRepo),
|
|
486
|
+
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
487
|
+
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
488
|
+
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
489
|
+
};
|
|
490
|
+
} catch {
|
|
491
|
+
return {};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function parsePendingDevice(value) {
|
|
495
|
+
if (!value || typeof value !== "object")
|
|
496
|
+
return null;
|
|
497
|
+
const record = value;
|
|
498
|
+
const pollId = cleanString(record.pollId);
|
|
499
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
500
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
501
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
502
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
503
|
+
return null;
|
|
504
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
505
|
+
}
|
|
506
|
+
function writeStoredAuth(stateFile, payload) {
|
|
507
|
+
mkdirSync3(resolve6(stateFile, ".."), { recursive: true });
|
|
508
|
+
writeFileSync3(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
509
|
+
`, { encoding: "utf8", mode: 384 });
|
|
510
|
+
try {
|
|
511
|
+
chmodSync(stateFile, 384);
|
|
512
|
+
} catch {}
|
|
513
|
+
}
|
|
514
|
+
function resolveGitHubAuthStateFile(projectRoot) {
|
|
515
|
+
return resolve6(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
516
|
+
}
|
|
517
|
+
function createGitHubAuthStore(projectRoot) {
|
|
518
|
+
const stateFile = resolveGitHubAuthStateFile(projectRoot);
|
|
519
|
+
return {
|
|
520
|
+
stateFile,
|
|
521
|
+
status(options) {
|
|
522
|
+
const stored = readStoredAuth(stateFile);
|
|
523
|
+
const token = cleanString(stored.token);
|
|
524
|
+
return {
|
|
525
|
+
signedIn: Boolean(token),
|
|
526
|
+
login: cleanString(stored.login),
|
|
527
|
+
userId: cleanString(stored.userId),
|
|
528
|
+
scopes: cleanScopes(stored.scopes),
|
|
529
|
+
selectedRepo: cleanString(stored.selectedRepo),
|
|
530
|
+
oauthConfigured: options?.oauthConfigured === true,
|
|
531
|
+
tokenSource: token ? stored.tokenSource ?? "manual-token" : null
|
|
532
|
+
};
|
|
533
|
+
},
|
|
534
|
+
readToken() {
|
|
535
|
+
return cleanString(readStoredAuth(stateFile).token);
|
|
536
|
+
},
|
|
537
|
+
saveToken(input) {
|
|
538
|
+
const previous = readStoredAuth(stateFile);
|
|
539
|
+
writeStoredAuth(stateFile, {
|
|
540
|
+
...previous,
|
|
541
|
+
token: input.token,
|
|
542
|
+
tokenSource: input.tokenSource,
|
|
543
|
+
login: input.login ?? null,
|
|
544
|
+
userId: input.userId ?? null,
|
|
545
|
+
scopes: input.scopes ?? [],
|
|
546
|
+
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
547
|
+
pendingDevice: null,
|
|
548
|
+
updatedAt: new Date().toISOString()
|
|
549
|
+
});
|
|
550
|
+
},
|
|
551
|
+
savePendingDevice(input) {
|
|
552
|
+
const previous = readStoredAuth(stateFile);
|
|
553
|
+
writeStoredAuth(stateFile, {
|
|
554
|
+
...previous,
|
|
555
|
+
pendingDevice: input,
|
|
556
|
+
updatedAt: new Date().toISOString()
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
saveSelectedRepo(selectedRepo) {
|
|
560
|
+
const previous = readStoredAuth(stateFile);
|
|
561
|
+
writeStoredAuth(stateFile, {
|
|
562
|
+
...previous,
|
|
563
|
+
selectedRepo: selectedRepo ?? null,
|
|
564
|
+
updatedAt: new Date().toISOString()
|
|
565
|
+
});
|
|
566
|
+
},
|
|
567
|
+
readPendingDevice(pollId) {
|
|
568
|
+
const pending = readStoredAuth(stateFile).pendingDevice ?? null;
|
|
569
|
+
if (!pending || pending.pollId !== pollId)
|
|
570
|
+
return null;
|
|
571
|
+
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
572
|
+
return null;
|
|
573
|
+
return pending;
|
|
574
|
+
},
|
|
575
|
+
clearPendingDevice() {
|
|
576
|
+
const previous = readStoredAuth(stateFile);
|
|
577
|
+
writeStoredAuth(stateFile, {
|
|
578
|
+
...previous,
|
|
579
|
+
pendingDevice: null,
|
|
580
|
+
updatedAt: new Date().toISOString()
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// packages/server/src/server-helpers/github-projects.ts
|
|
587
|
+
function asRecord(value) {
|
|
588
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
589
|
+
}
|
|
590
|
+
function asString(value) {
|
|
591
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
592
|
+
}
|
|
593
|
+
async function defaultGraphQLFetch(query, variables, token) {
|
|
594
|
+
const response = await fetch("https://api.github.com/graphql", {
|
|
595
|
+
method: "POST",
|
|
596
|
+
headers: {
|
|
597
|
+
"content-type": "application/json",
|
|
598
|
+
authorization: `Bearer ${token}`,
|
|
599
|
+
accept: "application/vnd.github+json"
|
|
600
|
+
},
|
|
601
|
+
body: JSON.stringify({ query, variables })
|
|
602
|
+
});
|
|
603
|
+
const json = await response.json().catch(() => ({}));
|
|
604
|
+
if (!response.ok || json.errors) {
|
|
605
|
+
throw new Error(`GitHub Projects GraphQL request failed: ${JSON.stringify(json.errors ?? { status: response.status })}`);
|
|
606
|
+
}
|
|
607
|
+
return json.data;
|
|
608
|
+
}
|
|
609
|
+
async function resolveProjectStatusField(input) {
|
|
610
|
+
const query = `
|
|
611
|
+
query RigProjectStatusField($projectId: ID!) {
|
|
612
|
+
node(id: $projectId) {
|
|
613
|
+
... on ProjectV2 {
|
|
614
|
+
fields(first: 50) {
|
|
615
|
+
nodes {
|
|
616
|
+
... on ProjectV2FieldCommon { id name }
|
|
617
|
+
... on ProjectV2SingleSelectField { id name options { id name } }
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
`;
|
|
624
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
625
|
+
const data = await fetchGraphQL(query, { projectId: input.projectId }, input.token);
|
|
626
|
+
const fields = asRecord(asRecord(asRecord(data)?.node)?.fields)?.nodes;
|
|
627
|
+
for (const node of Array.isArray(fields) ? fields : []) {
|
|
628
|
+
const record = asRecord(node);
|
|
629
|
+
if (asString(record?.name)?.toLowerCase() !== "status")
|
|
630
|
+
continue;
|
|
631
|
+
const id = asString(record?.id);
|
|
632
|
+
if (!id)
|
|
633
|
+
continue;
|
|
634
|
+
const options = Array.isArray(record?.options) ? record.options.flatMap((option) => {
|
|
635
|
+
const optionRecord = asRecord(option);
|
|
636
|
+
const optionId = asString(optionRecord?.id);
|
|
637
|
+
const name = asString(optionRecord?.name);
|
|
638
|
+
return optionId && name ? [{ id: optionId, name }] : [];
|
|
639
|
+
}) : [];
|
|
640
|
+
return { id, name: "Status", options };
|
|
641
|
+
}
|
|
642
|
+
throw new Error(`GitHub Project ${input.projectId} does not expose a Status single-select field.`);
|
|
643
|
+
}
|
|
644
|
+
async function ensureIssueProjectItem(input) {
|
|
645
|
+
const query = `
|
|
646
|
+
query RigFindProjectIssueItem($projectId: ID!, $issueNodeId: ID!) {
|
|
647
|
+
node(id: $projectId) {
|
|
648
|
+
... on ProjectV2 {
|
|
649
|
+
items(first: 100) { nodes { id content { ... on Issue { id } } } }
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
`;
|
|
654
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
655
|
+
const data = await fetchGraphQL(query, { projectId: input.projectId, issueNodeId: input.issueNodeId }, input.token);
|
|
656
|
+
const nodes = asRecord(asRecord(asRecord(data)?.node)?.items)?.nodes;
|
|
657
|
+
for (const node of Array.isArray(nodes) ? nodes : []) {
|
|
658
|
+
const record = asRecord(node);
|
|
659
|
+
const content = asRecord(record?.content);
|
|
660
|
+
if (asString(content?.id) === input.issueNodeId) {
|
|
661
|
+
const id2 = asString(record?.id);
|
|
662
|
+
if (id2)
|
|
663
|
+
return { id: id2, created: false };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const mutation = `
|
|
667
|
+
mutation RigAddIssueToProject($projectId: ID!, $contentId: ID!) {
|
|
668
|
+
addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) { item { id } }
|
|
669
|
+
}
|
|
670
|
+
`;
|
|
671
|
+
const created = await fetchGraphQL(mutation, { projectId: input.projectId, contentId: input.issueNodeId }, input.token);
|
|
672
|
+
const addResult = asRecord(asRecord(created)?.addProjectV2ItemById);
|
|
673
|
+
const id = asString(asRecord(addResult?.item)?.id);
|
|
674
|
+
if (!id)
|
|
675
|
+
throw new Error("GitHub Project item creation did not return an item id.");
|
|
676
|
+
return { id, created: true };
|
|
677
|
+
}
|
|
678
|
+
async function updateIssueProjectStatus(input) {
|
|
679
|
+
const mutation = `
|
|
680
|
+
mutation RigUpdateProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
681
|
+
updateProjectV2ItemFieldValue(input: {
|
|
682
|
+
projectId: $projectId,
|
|
683
|
+
itemId: $itemId,
|
|
684
|
+
fieldId: $fieldId,
|
|
685
|
+
value: { singleSelectOptionId: $optionId }
|
|
686
|
+
}) { projectV2Item { id } }
|
|
687
|
+
}
|
|
688
|
+
`;
|
|
689
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
690
|
+
await fetchGraphQL(mutation, {
|
|
691
|
+
projectId: input.projectId,
|
|
692
|
+
itemId: input.itemId,
|
|
693
|
+
fieldId: input.fieldId,
|
|
694
|
+
optionId: input.optionId
|
|
695
|
+
}, input.token);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// packages/server/src/server-helpers/github-project-status-sync.ts
|
|
699
|
+
var DEFAULT_PROJECT_STATUSES = {
|
|
700
|
+
running: "In Progress",
|
|
701
|
+
prOpen: "In Review",
|
|
702
|
+
ciFixing: "In Review",
|
|
703
|
+
done: "Done",
|
|
704
|
+
needsAttention: "Needs Attention"
|
|
705
|
+
};
|
|
706
|
+
function lifecycleStatusForTaskStatus(status) {
|
|
707
|
+
const normalized = status?.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
708
|
+
if (!normalized)
|
|
709
|
+
return null;
|
|
710
|
+
if (normalized === "closed" || normalized === "done")
|
|
711
|
+
return "done";
|
|
712
|
+
if (normalized === "under_review" || normalized === "review" || normalized === "pr_open")
|
|
713
|
+
return "prOpen";
|
|
714
|
+
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
715
|
+
return "ciFixing";
|
|
716
|
+
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
717
|
+
return "needsAttention";
|
|
718
|
+
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
719
|
+
return "running";
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
function cleanString2(value) {
|
|
723
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
724
|
+
}
|
|
725
|
+
function projectConfigFrom(config) {
|
|
726
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
727
|
+
return null;
|
|
728
|
+
const root = config;
|
|
729
|
+
return root.github?.projects ?? null;
|
|
730
|
+
}
|
|
731
|
+
async function syncGitHubProjectStatusForTaskUpdate(input) {
|
|
732
|
+
const projects = projectConfigFrom(input.config);
|
|
733
|
+
if (!projects?.enabled)
|
|
734
|
+
return { synced: false, reason: "project-sync-disabled" };
|
|
735
|
+
const projectId = cleanString2(projects.projectId);
|
|
736
|
+
if (!projectId)
|
|
737
|
+
return { synced: false, reason: "missing-project-id" };
|
|
738
|
+
const token = cleanString2(input.token);
|
|
739
|
+
if (!token)
|
|
740
|
+
return { synced: false, reason: "missing-token" };
|
|
741
|
+
const lifecycleStatus = lifecycleStatusForTaskStatus(input.status);
|
|
742
|
+
if (!lifecycleStatus)
|
|
743
|
+
return { synced: false, reason: "missing-status" };
|
|
744
|
+
const issueNodeId = cleanString2(input.issueNodeId);
|
|
745
|
+
if (!issueNodeId)
|
|
746
|
+
return { synced: false, reason: "missing-issue-node-id" };
|
|
747
|
+
const projectStatus = cleanString2(projects.statuses?.[lifecycleStatus]) ?? DEFAULT_PROJECT_STATUSES[lifecycleStatus];
|
|
748
|
+
const field = await resolveProjectStatusField({ projectId, token, fetchGraphQL: input.fetchGraphQL });
|
|
749
|
+
const option = field.options.find((candidate) => candidate.name.toLowerCase() === projectStatus.toLowerCase() || candidate.id === projectStatus);
|
|
750
|
+
if (!option)
|
|
751
|
+
return { synced: false, reason: "missing-project-status-option" };
|
|
752
|
+
const item = await ensureIssueProjectItem({ projectId, issueNodeId, token, fetchGraphQL: input.fetchGraphQL });
|
|
753
|
+
await updateIssueProjectStatus({
|
|
754
|
+
projectId,
|
|
755
|
+
itemId: item.id,
|
|
756
|
+
fieldId: cleanString2(projects.statusFieldId) ?? field.id,
|
|
757
|
+
optionId: option.id,
|
|
758
|
+
token,
|
|
759
|
+
fetchGraphQL: input.fetchGraphQL
|
|
760
|
+
});
|
|
761
|
+
return { synced: true, lifecycleStatus, projectStatus, itemId: item.id };
|
|
762
|
+
}
|
|
763
|
+
function extractGitHubIssueNodeId(task) {
|
|
764
|
+
if (!task || typeof task !== "object" || Array.isArray(task))
|
|
765
|
+
return null;
|
|
766
|
+
const record = task;
|
|
767
|
+
const direct = cleanString2(record.issueNodeId) ?? cleanString2(record.nodeId) ?? cleanString2(record.node_id);
|
|
768
|
+
if (direct)
|
|
769
|
+
return direct;
|
|
770
|
+
const raw = record.raw;
|
|
771
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
772
|
+
return null;
|
|
773
|
+
const rawRecord = raw;
|
|
774
|
+
return cleanString2(rawRecord.id) ?? cleanString2(rawRecord.nodeId) ?? cleanString2(rawRecord.node_id);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// packages/server/src/server-helpers/http-router.ts
|
|
778
|
+
var RIG_CONFIG_PACKAGE_VERSION = "0.0.6-alpha.0";
|
|
779
|
+
var RIG_CONFIG_DEV_DEPENDENCIES = {
|
|
780
|
+
"@rig/core": `npm:@h-rig/core@${RIG_CONFIG_PACKAGE_VERSION}`,
|
|
781
|
+
"@rig/standard-plugin": `npm:@h-rig/standard-plugin@${RIG_CONFIG_PACKAGE_VERSION}`
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// packages/server/src/server-helpers/ws-router.ts
|
|
785
|
+
import {
|
|
786
|
+
ORCHESTRATION_WS_CHANNELS,
|
|
787
|
+
ORCHESTRATION_WS_METHODS,
|
|
788
|
+
RIG_WS_METHODS,
|
|
789
|
+
WS_METHODS
|
|
790
|
+
} from "@rig/contracts";
|
|
791
|
+
import {
|
|
792
|
+
listManagedRemoteEndpoints as listManagedRemoteEndpoints2,
|
|
793
|
+
removeManagedRemoteEndpoint as removeManagedRemoteEndpoint2,
|
|
794
|
+
resolveRemoteEndpoint as resolveRemoteEndpoint2,
|
|
795
|
+
upsertManagedRemoteEndpoint as upsertManagedRemoteEndpoint2,
|
|
796
|
+
RemoteWsClient as RemoteWsClient2
|
|
797
|
+
} from "@rig/runtime/control-plane/remote";
|
|
798
|
+
import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
|
|
799
|
+
import { readAuthorityRun as readAuthorityRun6 } from "@rig/runtime/control-plane/authority-files";
|
|
800
|
+
|
|
801
|
+
// packages/server/src/server-helpers/inspector-jobs.ts
|
|
802
|
+
import { readJsonFile as readJsonFile2 } from "@rig/runtime/control-plane/authority-files";
|
|
803
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot4 } from "@rig/runtime/control-plane/native/utils";
|
|
804
|
+
import { normalizeTaskLifecycleStatus as normalizeTaskLifecycleStatus2 } from "@rig/runtime/control-plane/state-sync/types";
|
|
805
|
+
|
|
806
|
+
// packages/server/src/inspector/discovery.ts
|
|
807
|
+
import {
|
|
808
|
+
runStatus
|
|
809
|
+
} from "@rig/runtime/control-plane/native/run-ops";
|
|
810
|
+
import {
|
|
811
|
+
listAuthorityRuns as listAuthorityRuns5,
|
|
812
|
+
readAuthorityRun as readAuthorityRun7
|
|
813
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
814
|
+
|
|
815
|
+
// packages/server/src/inspector/service.ts
|
|
816
|
+
var ACTIVE_RUN_STATUSES = new Set(["preparing", "running", "validating", "reviewing"]);
|
|
817
|
+
|
|
818
|
+
// packages/server/src/inspector/upstream-sync.ts
|
|
819
|
+
import { resolveMonorepoRoot as resolveMonorepoRoot3 } from "@rig/runtime/control-plane/native/utils";
|
|
820
|
+
var UPSTREAM_VALIDATION_DESCRIPTIONS = {
|
|
821
|
+
"integration:hg-auth-backport": "Preserves the upstream auth hardening cluster: nonce-backed node-client login, signature-aware JWT verification, shared-token onboarding semantics, and regression coverage.",
|
|
822
|
+
"integration:hg-core-security-backport": "Preserves the vendored core-app and backend security fixes for OTP validation, NoSQL/operator hardening, AES/encryption behavior, and rate-limit or redirect-state protections.",
|
|
823
|
+
"integration:hg-boundary-hardening": "Preserves boundary-safe CORS behavior and credential lookup error hygiene across the vendored backend and the extracted credentials-service analogue.",
|
|
824
|
+
"integration:hg-uudl-runtime": "Preserves the upstream uudl runtime/package fixes: production dependency placement, path-resolution/runtime bootstrap behavior, AWS/MSK environment detection, KMS coverage, and a compiling UUDL tree.",
|
|
825
|
+
"boundary:hg-auth-triage": "Requires a durable keep/adapt/reject triage record for the post-import auth policy and mobile-wallet SIWE commits so the watcher does not keep rediscovering the same ambiguity.",
|
|
826
|
+
"boundary:hg-upstream-triage": "Requires a durable keep/adapt/reject triage record for service-relevant upstream commits that do not match the current portable catalog, including commit hashes, touched paths, and follow-up routing decisions."
|
|
827
|
+
};
|
|
828
|
+
var CLUSTERS = {
|
|
829
|
+
"auth-hardening": {
|
|
830
|
+
classification: "portable-now",
|
|
831
|
+
title: "[HG-001] Preserve upstream auth and shared-token hardening across extracted auth work",
|
|
832
|
+
description: "Backport clearly portable auth changes introduced upstream after imported revision 58b56e15: 3196d5c (node-client login nonce), bb5b74f (JWT signature validation), and 92011ead (shared token verification for wallet signup onboarding). Apply them where they belong in this repo: hp-auth-service, hp-gateway if verification semantics surface there, and the upstream monorepo auth owner flows for the remaining wallet-login/shared-token work. This task must preserve the security behavior, not just copy code. It should explicitly reconcile with bd-k0y-10, bd-k0y-11, bd-k0y-12, and bd-k0y-13.",
|
|
833
|
+
acceptanceCriteria: "Map upstream commits 3196d5c, bb5b74f, and 92011ead to exact local targets; preserve nonce-backed node-client login, JWT signature validation, and shared-token verification behavior; add automated tests that fail without the hardening; document any deliberate non-ports.",
|
|
834
|
+
role: "extractor",
|
|
835
|
+
scope: [
|
|
836
|
+
"repos/spliter-monorepo/microservices/hp-auth-service/**",
|
|
837
|
+
"repos/spliter-monorepo/microservices/hp-gateway/**",
|
|
838
|
+
"repos/spliter-monorepo/humoongate/moongate/core-app/**"
|
|
839
|
+
],
|
|
840
|
+
validation: ["integration:hg-auth-backport", "boundary:changed-files"],
|
|
841
|
+
validationDescriptions: {
|
|
842
|
+
"integration:hg-auth-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-auth-backport"]
|
|
843
|
+
},
|
|
844
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:auth"]
|
|
845
|
+
},
|
|
846
|
+
"core-security": {
|
|
847
|
+
classification: "portable-now",
|
|
848
|
+
title: "[HG-002] Backport core-app and backend security fixes from post-import upstream",
|
|
849
|
+
description: "Backport the clearly portable security fixes from efdae709 (OTP validation bypass), f63830d (NoSQL operator injection hardening), d7f6919 (AES encryption), and af86ac16 (three critical/high vulnerabilities). Preserve the behavior in the vendored auth owner and the touched backend query surfaces instead of treating these as optional cleanups. The high-value touched files from af86ac16 must be reviewed and ported or consciously waived with rationale.",
|
|
850
|
+
acceptanceCriteria: "Review and port the exact security behaviors from efdae709, f63830d, d7f6919, and af86ac16; cover the listed core-app and backend touched files; add regression tests for OTP bypass, NoSQL operator injection, AES/encryption behavior, and rate-limit or redirect-state hardening; document any consciously waived upstream changes.",
|
|
851
|
+
role: "mechanic",
|
|
852
|
+
scope: [
|
|
853
|
+
"repos/spliter-monorepo/humoongate/moongate/core-app/**",
|
|
854
|
+
"repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**"
|
|
855
|
+
],
|
|
856
|
+
validation: ["integration:hg-core-security-backport", "boundary:changed-files"],
|
|
857
|
+
validationDescriptions: {
|
|
858
|
+
"integration:hg-core-security-backport": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-core-security-backport"]
|
|
859
|
+
},
|
|
860
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:security"]
|
|
861
|
+
},
|
|
862
|
+
"boundary-hardening": {
|
|
863
|
+
classification: "portable-now",
|
|
864
|
+
title: "[HG-003] Backport request-boundary hardening for CORS and credential lookup flows",
|
|
865
|
+
description: "Backport the clearly portable boundary hardening from 083bbe4 (CORS config) and 36d52fba (credential lookup error hygiene). Preserve request-boundary behavior in the vendored backend and any analogous extracted credential-facing service seams so attackers cannot learn internals from credential lookup failures or exploit inconsistent CORS policy.",
|
|
866
|
+
acceptanceCriteria: "Port the exact request-boundary behavior from 083bbe4 and 36d52fba; preserve safe CORS policy and credential lookup error hygiene; add tests showing callers do not receive overly detailed credential lookup failures; document any extracted-service analogue that must mirror the behavior.",
|
|
867
|
+
role: "mechanic",
|
|
868
|
+
scope: [
|
|
869
|
+
"repos/spliter-monorepo/humoongate/humanity/hp-backend-ts/**",
|
|
870
|
+
"repos/spliter-monorepo/microservices/hp-credentials-service/**"
|
|
871
|
+
],
|
|
872
|
+
validation: ["integration:hg-boundary-hardening", "boundary:changed-files"],
|
|
873
|
+
validationDescriptions: {
|
|
874
|
+
"integration:hg-boundary-hardening": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-boundary-hardening"]
|
|
875
|
+
},
|
|
876
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:boundary"]
|
|
877
|
+
},
|
|
878
|
+
"uudl-runtime": {
|
|
879
|
+
classification: "portable-now",
|
|
880
|
+
title: "[HG-004] Backport uudl runtime and packaging fixes from upstream",
|
|
881
|
+
description: "Backport the clearly portable uudl runtime fixes from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03. Preserve the runtime behavior rather than cherry-picking filenames: production dependency placement, tsconfig-paths resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS wiring all need to be reflected in whichever UUDL runtime this repo still owns.",
|
|
882
|
+
acceptanceCriteria: "Port the runtime behaviors from 2141b4c, 1fa56d7, a9a0a99, 9fcfa6e, and ea5cb03; preserve production dependency placement, tsconfig-paths runtime resolution, aggregator cycle removal, AWS/MSK environment detection, and local KMS configuration; add runtime-focused tests or smoke coverage for each preserved behavior.",
|
|
883
|
+
role: "extractor",
|
|
884
|
+
scope: ["repos/spliter-monorepo/humoongate/humanity/hp-uudl/**"],
|
|
885
|
+
validation: ["integration:hg-uudl-runtime", "boundary:changed-files"],
|
|
886
|
+
validationDescriptions: {
|
|
887
|
+
"integration:hg-uudl-runtime": UPSTREAM_VALIDATION_DESCRIPTIONS["integration:hg-uudl-runtime"]
|
|
888
|
+
},
|
|
889
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:uudl"]
|
|
890
|
+
},
|
|
891
|
+
"auth-triage": {
|
|
892
|
+
classification: "needs-human-triage",
|
|
893
|
+
title: "[HG-005] Triage post-import auth policy deltas and mobile-wallet SIWE changes",
|
|
894
|
+
description: "Review the upstream changes that are relevant but not safely auto-portable: 1262b75 (JWT_EXPIRES_IN 1d -> 7d), 15036424 (unlink-wallet endpoint and Wallet.default fix), and d4a47015 (unlink-wallet error handling). Produce an explicit keep/adapt/reject decision for each commit with rationale tied to the extracted auth plan, core-app ownership, and current threat model, and record the result in artifacts/bd-2ztk.5/decision-log.md so future agents do not silently re-litigate these decisions.",
|
|
895
|
+
acceptanceCriteria: "For 1262b75, 15036424, and d4a47015, produce explicit keep/adapt/reject decisions with rationale tied to auth ownership and threat model; record the decision in artifacts/bd-2ztk.5/decision-log.md; identify any follow-on implementation tasks if the answer is keep or adapt.",
|
|
896
|
+
role: "architect",
|
|
897
|
+
scope: ["artifacts/bd-2ztk.5/**", "docs/research/**"],
|
|
898
|
+
validation: ["boundary:hg-auth-triage"],
|
|
899
|
+
validationDescriptions: {
|
|
900
|
+
"boundary:hg-auth-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-auth-triage"]
|
|
901
|
+
},
|
|
902
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
|
|
903
|
+
},
|
|
904
|
+
"generic-triage": {
|
|
905
|
+
classification: "needs-human-triage",
|
|
906
|
+
title: "[HG-007] Triage uncatalogued service-relevant upstream commits",
|
|
907
|
+
description: "Review future upstream commits that touch service-relevant paths but do not match the current portable-now catalog. For each commit or batch, produce keep/adapt/reject decisions with rationale, cite the touched paths and why they matter here, and either route them into an existing backport task or open a new follow-up task when they are worth preserving.",
|
|
908
|
+
acceptanceCriteria: "For each uncatalogued service-relevant upstream commit or batch, record keep/adapt/reject decisions with rationale in artifacts/bd-2ztk.7/decision-log.md; cite the commit hashes and touched paths; and either attach the work to an existing backport task or open a new follow-up task when preservation is warranted.",
|
|
909
|
+
role: "architect",
|
|
910
|
+
scope: ["artifacts/bd-2ztk.7/**", "docs/research/**"],
|
|
911
|
+
validation: ["boundary:hg-upstream-triage"],
|
|
912
|
+
validationDescriptions: {
|
|
913
|
+
"boundary:hg-upstream-triage": UPSTREAM_VALIDATION_DESCRIPTIONS["boundary:hg-upstream-triage"]
|
|
914
|
+
},
|
|
915
|
+
labels: ["upstream:monorepo", "kind:backport", "cluster:triage"]
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// packages/server/src/server-helpers/task-config.ts
|
|
920
|
+
import { existsSync as existsSync4 } from "fs";
|
|
921
|
+
async function readTaskConfig(projectRoot) {
|
|
922
|
+
const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
|
|
923
|
+
if (!existsSync4(taskConfigPath)) {
|
|
924
|
+
return {};
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
const parsed = await Bun.file(taskConfigPath).json();
|
|
928
|
+
return parsed ?? {};
|
|
929
|
+
} catch {
|
|
930
|
+
return {};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// packages/server/src/server.ts
|
|
935
|
+
var RIG_WORKSPACE_ID = "rig-local-workspace";
|
|
936
|
+
var serverPathEnvQueue = Promise.resolve();
|
|
937
|
+
async function withServerPathEnv(projectRoot, fn) {
|
|
938
|
+
const waitForTurn = serverPathEnvQueue;
|
|
939
|
+
let releaseTurn;
|
|
940
|
+
serverPathEnvQueue = new Promise((resolve8) => {
|
|
941
|
+
releaseTurn = resolve8;
|
|
942
|
+
});
|
|
943
|
+
await waitForTurn;
|
|
944
|
+
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
945
|
+
const overrides = {
|
|
946
|
+
RIG_STATE_DIR: paths.stateDir,
|
|
947
|
+
RIG_LOGS_DIR: paths.logsDir,
|
|
948
|
+
RIG_SESSION_FILE: paths.sessionFile,
|
|
949
|
+
ARTIFACTS_DIR: paths.artifactsDir
|
|
950
|
+
};
|
|
951
|
+
const previous = new Map;
|
|
952
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
953
|
+
previous.set(key, process.env[key]);
|
|
954
|
+
process.env[key] = value;
|
|
955
|
+
}
|
|
956
|
+
try {
|
|
957
|
+
return await fn();
|
|
958
|
+
} finally {
|
|
959
|
+
for (const [key, value] of previous.entries()) {
|
|
960
|
+
if (value === undefined) {
|
|
961
|
+
delete process.env[key];
|
|
962
|
+
} else {
|
|
963
|
+
process.env[key] = value;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
releaseTurn();
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
|
|
970
|
+
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
971
|
+
if (process.env.RIG_STATE_DIR === paths.stateDir && process.env.RIG_LOGS_DIR === paths.logsDir && process.env.RIG_SESSION_FILE === paths.sessionFile) {
|
|
972
|
+
return await fn();
|
|
973
|
+
}
|
|
974
|
+
return withServerPathEnv(projectRoot, fn);
|
|
975
|
+
}
|
|
976
|
+
async function readWorkspaceTasks(projectRoot) {
|
|
977
|
+
const issuesPath = resolve7(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
|
|
978
|
+
const taskConfig = await readTaskConfig(projectRoot);
|
|
979
|
+
if (!existsSync5(issuesPath)) {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
const latestById = new Map;
|
|
983
|
+
for (const entry of readJsonlFile3(issuesPath).filter((candidate) => !!candidate && typeof candidate === "object" && !Array.isArray(candidate)).filter((candidate) => normalizeString(candidate.issue_type) !== "epic")) {
|
|
984
|
+
const id = normalizeString(entry.id) ?? "";
|
|
985
|
+
if (id.length === 0) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const config = taskConfig[id] ?? {};
|
|
989
|
+
const rawStatus = normalizeString(entry.status) ?? "open";
|
|
990
|
+
const normalizedStatus = normalizeTaskLifecycleStatus3(rawStatus);
|
|
991
|
+
const dependencyRecords = Array.isArray(entry.dependencies) ? entry.dependencies.filter((dependency) => !!dependency && typeof dependency === "object" && !Array.isArray(dependency)) : [];
|
|
992
|
+
latestById.set(id, {
|
|
993
|
+
id,
|
|
994
|
+
title: normalizeString(entry.title) ?? id,
|
|
995
|
+
description: normalizeString(entry.description),
|
|
996
|
+
acceptanceCriteria: normalizeString(entry.acceptance_criteria),
|
|
997
|
+
status: normalizedStatus ?? "unknown",
|
|
998
|
+
sourceStatus: normalizedStatus ? null : rawStatus,
|
|
999
|
+
priority: typeof entry.priority === "number" ? entry.priority : typeof entry.priority === "string" ? Number(entry.priority) : null,
|
|
1000
|
+
issueType: normalizeString(entry.issue_type),
|
|
1001
|
+
role: normalizeString(config.role) ?? null,
|
|
1002
|
+
externalRef: normalizeString(entry.external_ref) ?? normalizeString(config.wp_id) ?? null,
|
|
1003
|
+
sourceIssueId: normalizeString(entry.source_issue_id) ?? normalizeString(config.source_issue_id) ?? null,
|
|
1004
|
+
dependencies: dependencyRecords.filter((dependency) => normalizeString(dependency.type) === "blocks").map((dependency) => normalizeString(dependency.depends_on_id)).filter((dependency) => dependency !== null),
|
|
1005
|
+
parentChildDeps: dependencyRecords.filter((dependency) => normalizeString(dependency.type) === "parent-child").map((dependency) => normalizeString(dependency.depends_on_id)).filter((dependency) => dependency !== null),
|
|
1006
|
+
scope: normalizeStringArray(config.scope),
|
|
1007
|
+
validation: normalizeStringArray(config.validation),
|
|
1008
|
+
createdAt: normalizeString(entry.created_at),
|
|
1009
|
+
updatedAt: normalizeString(entry.updated_at),
|
|
1010
|
+
labels: normalizeStringArray(entry.labels)
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
return Array.from(latestById.values()).sort((left, right) => {
|
|
1014
|
+
const leftPriority = left.priority ?? Number.MAX_SAFE_INTEGER;
|
|
1015
|
+
const rightPriority = right.priority ?? Number.MAX_SAFE_INTEGER;
|
|
1016
|
+
if (leftPriority !== rightPriority) {
|
|
1017
|
+
return leftPriority - rightPriority;
|
|
1018
|
+
}
|
|
1019
|
+
return left.id.localeCompare(right.id);
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (false) {}
|
|
1023
|
+
|
|
1024
|
+
// packages/server/src/server-helpers/validation-failure.ts
|
|
1025
|
+
import { resolve as resolve8 } from "path";
|
|
1026
|
+
import {
|
|
1027
|
+
readJsonFile as readJsonFile4,
|
|
1028
|
+
resolveTaskArtifactDirs
|
|
1029
|
+
} from "@rig/runtime/control-plane/authority-files";
|
|
1030
|
+
function summarizeRunValidationFailure(projectRoot, run) {
|
|
1031
|
+
const artifactRoots = run.taskId ? resolveTaskArtifactDirs(projectRoot, run.taskId) : [];
|
|
1032
|
+
const preferredRoots = run.artifactRoot ? [run.artifactRoot, ...artifactRoots] : artifactRoots;
|
|
1033
|
+
const seen = new Set;
|
|
1034
|
+
for (const artifactRoot of preferredRoots) {
|
|
1035
|
+
if (!artifactRoot || seen.has(artifactRoot)) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
seen.add(artifactRoot);
|
|
1039
|
+
const summary = readJsonFile4(resolve8(artifactRoot, "validation-summary.json"), null);
|
|
1040
|
+
if (!summary || summary.status !== "fail") {
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const failedCategories = Array.isArray(summary.categories) ? summary.categories.filter((entry) => String(entry?.status ?? "") !== "pass").map((entry) => String(entry?.category ?? "").trim()).filter(Boolean) : [];
|
|
1044
|
+
if (failedCategories.length > 0) {
|
|
1045
|
+
return `Task validation failed: ${failedCategories.join(", ")}`;
|
|
1046
|
+
}
|
|
1047
|
+
return "Task validation failed.";
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// packages/server/src/server-helpers/run-mutations.ts
|
|
1053
|
+
async function enqueueRunLinearEvent(_projectRoot, _event) {}
|
|
1054
|
+
function fileExists(path) {
|
|
1055
|
+
try {
|
|
1056
|
+
return statSync3(path).isFile();
|
|
1057
|
+
} catch {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function sourceTaskContract(task) {
|
|
1062
|
+
return {
|
|
1063
|
+
id: task.id,
|
|
1064
|
+
title: task.title,
|
|
1065
|
+
description: task.description,
|
|
1066
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
1067
|
+
status: task.status,
|
|
1068
|
+
sourceStatus: task.sourceStatus,
|
|
1069
|
+
priority: task.priority,
|
|
1070
|
+
issueType: task.issueType,
|
|
1071
|
+
role: task.role,
|
|
1072
|
+
externalRef: task.externalRef,
|
|
1073
|
+
sourceIssueId: task.sourceIssueId,
|
|
1074
|
+
dependencies: task.dependencies,
|
|
1075
|
+
parentChildDeps: task.parentChildDeps,
|
|
1076
|
+
scope: task.scope,
|
|
1077
|
+
validation: task.validation,
|
|
1078
|
+
labels: task.labels,
|
|
1079
|
+
issueNodeId: task.issueNodeId ?? task.nodeId ?? null,
|
|
1080
|
+
raw: task.raw ?? null
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function runSourceTaskIdentity(run) {
|
|
1084
|
+
const sourceTask = run.sourceTask;
|
|
1085
|
+
return sourceTask && typeof sourceTask === "object" && !Array.isArray(sourceTask) ? sourceTask : null;
|
|
1086
|
+
}
|
|
1087
|
+
function lifecycleFailureMessage(taskId, status, error) {
|
|
1088
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
1089
|
+
return `Failed to update task source for ${taskId} to ${status}: ${details}`;
|
|
1090
|
+
}
|
|
1091
|
+
async function loadRigLifecycleConfig(projectRoot) {
|
|
1092
|
+
try {
|
|
1093
|
+
return await loadConfig(projectRoot);
|
|
1094
|
+
} catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function issueUpdatesMode(config) {
|
|
1099
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
1100
|
+
const mode = github?.issueUpdates;
|
|
1101
|
+
return mode === "off" || mode === "minimal" || mode === "lifecycle" ? mode : "lifecycle";
|
|
1102
|
+
}
|
|
1103
|
+
function shouldWriteIssueUpdate(mode, status) {
|
|
1104
|
+
if (mode === "off")
|
|
1105
|
+
return false;
|
|
1106
|
+
if (mode === "lifecycle")
|
|
1107
|
+
return true;
|
|
1108
|
+
return ["closed", "failed", "cancelled", "needs_attention"].includes(status);
|
|
1109
|
+
}
|
|
1110
|
+
function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
1111
|
+
const raw = normalizeString(sourceTask?.sourceIssueId) ?? normalizeString(sourceTask?.source_issue_id);
|
|
1112
|
+
const match = raw?.match(/^([^/\s]+)\/([^#\s]+)#(\d+)$/);
|
|
1113
|
+
if (match)
|
|
1114
|
+
return { owner: match[1], repo: match[2], number: match[3] };
|
|
1115
|
+
if (/^\d+$/.test(fallbackTaskId))
|
|
1116
|
+
return null;
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
1120
|
+
if (!run.taskId)
|
|
1121
|
+
return;
|
|
1122
|
+
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
1123
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
1124
|
+
taskId: run.taskId,
|
|
1125
|
+
status,
|
|
1126
|
+
issueNodeId,
|
|
1127
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
1128
|
+
config
|
|
1129
|
+
}).catch(() => {
|
|
1130
|
+
return;
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
async function autoAssignRunIssue(projectRoot, run) {
|
|
1134
|
+
if (!run.taskId)
|
|
1135
|
+
return;
|
|
1136
|
+
const store = createGitHubAuthStore(projectRoot);
|
|
1137
|
+
const token = store.readToken();
|
|
1138
|
+
const login = normalizeString(run.initiatedBy) ?? store.status().login;
|
|
1139
|
+
if (!token || !login)
|
|
1140
|
+
return;
|
|
1141
|
+
const ref = parseIssueRef(runSourceTaskIdentity(run), run.taskId);
|
|
1142
|
+
if (!ref)
|
|
1143
|
+
return;
|
|
1144
|
+
await fetch(`https://api.github.com/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/assignees`, {
|
|
1145
|
+
method: "POST",
|
|
1146
|
+
headers: {
|
|
1147
|
+
accept: "application/vnd.github+json",
|
|
1148
|
+
authorization: `Bearer ${token}`,
|
|
1149
|
+
"content-type": "application/json",
|
|
1150
|
+
"user-agent": "rig-server"
|
|
1151
|
+
},
|
|
1152
|
+
body: JSON.stringify({ assignees: [login] })
|
|
1153
|
+
}).catch(() => {
|
|
1154
|
+
return;
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, options = {}) {
|
|
1158
|
+
if (!run.taskId) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const config = await loadRigLifecycleConfig(projectRoot);
|
|
1162
|
+
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
1163
|
+
if (status === "in_progress") {
|
|
1164
|
+
await autoAssignRunIssue(projectRoot, run);
|
|
1165
|
+
}
|
|
1166
|
+
const mode = issueUpdatesMode(config);
|
|
1167
|
+
if (!shouldWriteIssueUpdate(mode, status)) {
|
|
1168
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
1169
|
+
id: `log:${run.runId}:task-source-update-skipped:${status}`,
|
|
1170
|
+
title: "Task source lifecycle update skipped",
|
|
1171
|
+
detail: `github.issueUpdates=${mode}; ${status} issue update skipped.`,
|
|
1172
|
+
tone: "info",
|
|
1173
|
+
status: "running",
|
|
1174
|
+
createdAt: new Date().toISOString()
|
|
1175
|
+
});
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const result = await updateConfiguredTaskSourceTask2(projectRoot, {
|
|
1179
|
+
taskId: run.taskId,
|
|
1180
|
+
sourceTask: runSourceTaskIdentity(run),
|
|
1181
|
+
update: {
|
|
1182
|
+
status,
|
|
1183
|
+
comment: buildTaskRunLifecycleComment2({
|
|
1184
|
+
runId: run.runId,
|
|
1185
|
+
status,
|
|
1186
|
+
summary,
|
|
1187
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
1188
|
+
logsDir: normalizeString(run.logRoot),
|
|
1189
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
1190
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
1191
|
+
})
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
if (!result.updated) {
|
|
1195
|
+
if (result.source === "plugin" || result.sourceKind) {
|
|
1196
|
+
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
1197
|
+
}
|
|
1198
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
1199
|
+
id: `log:${run.runId}:task-source-unconfigured:${status}`,
|
|
1200
|
+
title: "Task source lifecycle update skipped",
|
|
1201
|
+
detail: `No source-aware task source configured for ${result.taskId}; ${status} update skipped.`,
|
|
1202
|
+
tone: "info",
|
|
1203
|
+
status: "running",
|
|
1204
|
+
createdAt: new Date().toISOString()
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
1209
|
+
"completed",
|
|
1210
|
+
"complete",
|
|
1211
|
+
"done",
|
|
1212
|
+
"merged",
|
|
1213
|
+
"closed",
|
|
1214
|
+
"failed",
|
|
1215
|
+
"cancelled",
|
|
1216
|
+
"canceled",
|
|
1217
|
+
"needs_attention",
|
|
1218
|
+
"needs-attention",
|
|
1219
|
+
"stopped"
|
|
1220
|
+
]);
|
|
1221
|
+
function isActiveRunForTask(run, taskId, newRunId) {
|
|
1222
|
+
if (run.runId === newRunId || run.taskId !== taskId)
|
|
1223
|
+
return false;
|
|
1224
|
+
const normalizedStatus = normalizeString(run.status)?.toLowerCase() ?? "created";
|
|
1225
|
+
return !TERMINAL_RUN_STATUSES2.has(normalizedStatus);
|
|
1226
|
+
}
|
|
1227
|
+
function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
1228
|
+
const existing = listAuthorityRuns7(projectRoot).find((run) => isActiveRunForTask(run, taskId, newRunId));
|
|
1229
|
+
if (!existing)
|
|
1230
|
+
return;
|
|
1231
|
+
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
1232
|
+
}
|
|
1233
|
+
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
1234
|
+
if ("taskId" in input && input.taskId) {
|
|
1235
|
+
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
1236
|
+
}
|
|
1237
|
+
const sourceTask = "taskId" in input && input.taskId ? (await readTasks(projectRoot)).find((task) => task.id === input.taskId) ?? null : null;
|
|
1238
|
+
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
1239
|
+
const runDir = resolveAuthorityRunDir4(projectRoot, input.runId);
|
|
1240
|
+
const runRecord = {
|
|
1241
|
+
runId: input.runId,
|
|
1242
|
+
projectRoot,
|
|
1243
|
+
workspaceId: input.workspaceId,
|
|
1244
|
+
taskId: "taskId" in input ? input.taskId ?? null : null,
|
|
1245
|
+
threadId: null,
|
|
1246
|
+
mode: input.executionTarget === "remote" ? "remote" : "local",
|
|
1247
|
+
runtimeAdapter: normalizeRuntimeAdapter(input.runtimeAdapter),
|
|
1248
|
+
status: "created",
|
|
1249
|
+
createdAt: input.createdAt,
|
|
1250
|
+
startedAt: null,
|
|
1251
|
+
completedAt: null,
|
|
1252
|
+
endpointId: null,
|
|
1253
|
+
hostId: input.remoteHostId ?? null,
|
|
1254
|
+
worktreePath: null,
|
|
1255
|
+
artifactRoot: null,
|
|
1256
|
+
logRoot: null,
|
|
1257
|
+
sessionPath: null,
|
|
1258
|
+
sessionLogPath: null,
|
|
1259
|
+
updatedAt: input.createdAt,
|
|
1260
|
+
title: "title" in input && input.title ? input.title : taskTitle ?? "New run",
|
|
1261
|
+
model: input.model ?? null,
|
|
1262
|
+
runtimeMode: input.runtimeMode ?? "full-access",
|
|
1263
|
+
interactionMode: input.interactionMode ?? "default",
|
|
1264
|
+
runMode: (input.runtimeMode ?? "full-access") === "full-access" ? "autonomous" : "interactive",
|
|
1265
|
+
initialPrompt: "initialPrompt" in input ? input.initialPrompt ?? null : null,
|
|
1266
|
+
baselineMode: input.baselineMode ?? "head",
|
|
1267
|
+
prMode: input.prMode ?? null,
|
|
1268
|
+
initiatedBy: input.initiatedBy ?? null,
|
|
1269
|
+
...sourceTask ? { sourceTask: sourceTaskContract(sourceTask) } : {}
|
|
1270
|
+
};
|
|
1271
|
+
mkdirSync5(runDir, { recursive: true });
|
|
1272
|
+
writeFileSync5(resolve9(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
|
|
1273
|
+
`, "utf8");
|
|
1274
|
+
if ("initialPrompt" in input && input.initialPrompt && input.initialPrompt.trim().length > 0) {
|
|
1275
|
+
writeFileSync5(resolve9(runDir, "timeline.jsonl"), `${JSON.stringify({
|
|
1276
|
+
id: `message-${Date.now()}`,
|
|
1277
|
+
type: "user_message",
|
|
1278
|
+
text: input.initialPrompt,
|
|
1279
|
+
createdAt: input.createdAt
|
|
1280
|
+
})}
|
|
1281
|
+
`, "utf8");
|
|
1282
|
+
}
|
|
1283
|
+
if (runRecord.taskId) {
|
|
1284
|
+
const taskId = runRecord.taskId;
|
|
1285
|
+
(async () => {
|
|
1286
|
+
try {
|
|
1287
|
+
await updateRunTaskSourceLifecycle(projectRoot, runRecord, "in_progress", "Rig task run started and claimed this task.");
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
const failureSummary = lifecycleFailureMessage(taskId, "in_progress", error);
|
|
1290
|
+
appendRunLogEntry(projectRoot, input.runId, {
|
|
1291
|
+
id: `log:${input.runId}:task-source-in-progress-update`,
|
|
1292
|
+
title: "Task source claim update failed",
|
|
1293
|
+
detail: failureSummary,
|
|
1294
|
+
tone: "error",
|
|
1295
|
+
status: "running",
|
|
1296
|
+
createdAt: new Date().toISOString(),
|
|
1297
|
+
payload: { taskId }
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
})();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async function startLocalRun(state, runId, options) {
|
|
1304
|
+
const run = readAuthorityRun8(state.projectRoot, runId);
|
|
1305
|
+
if (!run) {
|
|
1306
|
+
throw new Error(`Run not found: ${runId}`);
|
|
1307
|
+
}
|
|
1308
|
+
const startedAt = new Date().toISOString();
|
|
1309
|
+
state.runProcesses.set(runId, {
|
|
1310
|
+
runId,
|
|
1311
|
+
child: null,
|
|
1312
|
+
startedAt,
|
|
1313
|
+
stopped: false
|
|
1314
|
+
});
|
|
1315
|
+
patchRunRecord(state.projectRoot, runId, buildRunStartPatch(startedAt));
|
|
1316
|
+
await enqueueRunLinearEvent(state.projectRoot, {
|
|
1317
|
+
type: "run.started",
|
|
1318
|
+
runId,
|
|
1319
|
+
taskId: run.taskId,
|
|
1320
|
+
timestamp: startedAt,
|
|
1321
|
+
agent: run.runtimeAdapter,
|
|
1322
|
+
summary: run.title
|
|
1323
|
+
});
|
|
1324
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
1325
|
+
id: `log:${runId}:prepare`,
|
|
1326
|
+
title: "Rig task run starting",
|
|
1327
|
+
detail: run.taskId ?? run.title,
|
|
1328
|
+
tone: "info",
|
|
1329
|
+
status: "preparing",
|
|
1330
|
+
createdAt: startedAt
|
|
1331
|
+
});
|
|
1332
|
+
broadcastRunLogAppended(state, runId, readLatestRawRunLog(state.projectRoot, runId));
|
|
1333
|
+
broadcastSnapshotInvalidation(state);
|
|
1334
|
+
const cliProjectRoot = resolveLocalRunCliProjectRoot(state.projectRoot);
|
|
1335
|
+
const cliEntryPoint = resolve9(cliProjectRoot, "packages/cli/bin/rig.ts");
|
|
1336
|
+
if (!existsSync6(cliEntryPoint)) {
|
|
1337
|
+
const completedAt = new Date().toISOString();
|
|
1338
|
+
const failureSummary = `Rig task-run entrypoint missing at ${relative2(state.projectRoot, cliEntryPoint)}`;
|
|
1339
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1340
|
+
status: "failed",
|
|
1341
|
+
completedAt,
|
|
1342
|
+
errorText: failureSummary
|
|
1343
|
+
});
|
|
1344
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1345
|
+
id: `log:${runId}:missing-entrypoint`,
|
|
1346
|
+
title: "Run failed",
|
|
1347
|
+
detail: failureSummary,
|
|
1348
|
+
tone: "error",
|
|
1349
|
+
status: "failed",
|
|
1350
|
+
createdAt: completedAt
|
|
1351
|
+
}, "local-run-missing-entrypoint");
|
|
1352
|
+
await enqueueRunLinearEvent(state.projectRoot, {
|
|
1353
|
+
type: "run.failed",
|
|
1354
|
+
runId,
|
|
1355
|
+
taskId: run.taskId,
|
|
1356
|
+
timestamp: completedAt,
|
|
1357
|
+
agent: run.runtimeAdapter,
|
|
1358
|
+
summary: failureSummary
|
|
1359
|
+
});
|
|
1360
|
+
state.runProcesses.delete(runId);
|
|
1361
|
+
broadcastSnapshotInvalidation(state);
|
|
1362
|
+
await reconcileScheduler(state, "local-run-missing-entrypoint");
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
queueMicrotask(() => {
|
|
1366
|
+
withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
|
|
1367
|
+
const command = [
|
|
1368
|
+
"bun",
|
|
1369
|
+
"run",
|
|
1370
|
+
cliEntryPoint,
|
|
1371
|
+
"--run-id",
|
|
1372
|
+
runId,
|
|
1373
|
+
"server",
|
|
1374
|
+
"task-run",
|
|
1375
|
+
...run.taskId ? ["--task", run.taskId] : [],
|
|
1376
|
+
...normalizeString(run.title) ? ["--title", normalizeString(run.title)] : [],
|
|
1377
|
+
"--runtime-adapter",
|
|
1378
|
+
normalizeRuntimeAdapter(run.runtimeAdapter),
|
|
1379
|
+
...normalizeString(run.model) ? ["--model", normalizeString(run.model)] : [],
|
|
1380
|
+
"--runtime-mode",
|
|
1381
|
+
normalizeString(run.runtimeMode) ?? "full-access",
|
|
1382
|
+
"--interaction-mode",
|
|
1383
|
+
normalizeString(run.interactionMode) ?? "default",
|
|
1384
|
+
...normalizeString(options?.promptOverride) ?? normalizeString(run.initialPrompt) ? [
|
|
1385
|
+
"--initial-prompt",
|
|
1386
|
+
normalizeString(options?.promptOverride) ?? normalizeString(run.initialPrompt)
|
|
1387
|
+
] : [],
|
|
1388
|
+
...normalizeString(run.baselineMode) ? ["--dirty-baseline", normalizeString(run.baselineMode)] : [],
|
|
1389
|
+
...normalizeString(run.prMode) ? ["--pr", normalizeString(run.prMode)] : []
|
|
1390
|
+
];
|
|
1391
|
+
const publishedServer = readPublishedRigServerStateSync(state.projectRoot);
|
|
1392
|
+
const serverUrl = process.env.RIG_SERVER_URL?.trim() || (publishedServer ? `http://${publishedServer.host}:${publishedServer.port}` : "");
|
|
1393
|
+
const bridgeAuthToken = process.env.RIG_AUTH_TOKEN?.trim() || publishedServer?.authToken || "";
|
|
1394
|
+
const bridgeGitHubToken = process.env.RIG_GITHUB_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || createGitHubAuthStore(state.projectRoot).readToken() || "";
|
|
1395
|
+
const child = spawn(command[0], command.slice(1), {
|
|
1396
|
+
cwd: cliProjectRoot,
|
|
1397
|
+
env: {
|
|
1398
|
+
...process.env,
|
|
1399
|
+
PROJECT_RIG_ROOT: state.projectRoot,
|
|
1400
|
+
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
1401
|
+
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
1402
|
+
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
1403
|
+
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
1404
|
+
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
1405
|
+
...bridgeGitHubToken ? { RIG_GITHUB_TOKEN: bridgeGitHubToken } : {}
|
|
1406
|
+
},
|
|
1407
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1408
|
+
});
|
|
1409
|
+
const processState = state.runProcesses.get(runId);
|
|
1410
|
+
if (processState) {
|
|
1411
|
+
processState.child = child;
|
|
1412
|
+
}
|
|
1413
|
+
const handleRunProcessOutput = (detail, tone, title) => {
|
|
1414
|
+
for (const rawLine of detail.split(/\r?\n/)) {
|
|
1415
|
+
const line = rawLine.trim();
|
|
1416
|
+
if (!line)
|
|
1417
|
+
continue;
|
|
1418
|
+
if (line.startsWith("__RIG_RUN_EVENT__")) {
|
|
1419
|
+
try {
|
|
1420
|
+
const serverEvent = JSON.parse(line.slice("__RIG_RUN_EVENT__".length));
|
|
1421
|
+
if (serverEvent.type === "log" && serverEvent.runId) {
|
|
1422
|
+
broadcastRunLogAppended(state, serverEvent.runId, readLatestRawRunLog(state.projectRoot, serverEvent.runId));
|
|
1423
|
+
}
|
|
1424
|
+
} catch {}
|
|
1425
|
+
broadcastSnapshotInvalidation(state);
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1429
|
+
id: `log:${runId}:${Date.now()}`,
|
|
1430
|
+
title,
|
|
1431
|
+
detail: line,
|
|
1432
|
+
tone,
|
|
1433
|
+
status: "running",
|
|
1434
|
+
createdAt: new Date().toISOString()
|
|
1435
|
+
}, "run-log-line");
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
child.stdout.on("data", (data) => {
|
|
1439
|
+
handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "info", "Rig task run");
|
|
1440
|
+
});
|
|
1441
|
+
child.stderr.on("data", (data) => {
|
|
1442
|
+
handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
|
|
1443
|
+
});
|
|
1444
|
+
try {
|
|
1445
|
+
const exit = await new Promise((resolve10) => {
|
|
1446
|
+
child.once("error", (error) => resolve10({ code: 1, signal: null, error }));
|
|
1447
|
+
child.once("close", (code, signal) => resolve10({ code, signal }));
|
|
1448
|
+
});
|
|
1449
|
+
if (exit.error) {
|
|
1450
|
+
throw new Error(`Failed to start task run: ${exit.error.message}`);
|
|
1451
|
+
}
|
|
1452
|
+
const current = readAuthorityRun8(state.projectRoot, runId);
|
|
1453
|
+
if (!current) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
|
|
1457
|
+
const completedAt = current.completedAt ?? new Date().toISOString();
|
|
1458
|
+
const failureSummary = normalizeString(current.errorText) ?? summarizeRunValidationFailure(state.projectRoot, current) ?? `Rig task-run exited with code ${String(exit.code ?? "unknown")}`;
|
|
1459
|
+
if (current.status !== "failed") {
|
|
1460
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1461
|
+
status: "failed",
|
|
1462
|
+
completedAt,
|
|
1463
|
+
errorText: failureSummary
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
if (current.taskId) {
|
|
1467
|
+
try {
|
|
1468
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", completedAt, errorText: failureSummary }, "failed", "Rig task run failed.", { errorText: failureSummary });
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
const sourceFailure = lifecycleFailureMessage(current.taskId, "failed", error);
|
|
1471
|
+
patchRunRecord(state.projectRoot, runId, { errorText: `${failureSummary}
|
|
1472
|
+
${sourceFailure}` });
|
|
1473
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
1474
|
+
id: `log:${runId}:task-source-failed-update`,
|
|
1475
|
+
title: "Task source failure update failed",
|
|
1476
|
+
detail: sourceFailure,
|
|
1477
|
+
tone: "error",
|
|
1478
|
+
status: "failed",
|
|
1479
|
+
createdAt: new Date().toISOString()
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
await enqueueRunLinearEvent(state.projectRoot, {
|
|
1484
|
+
type: "run.failed",
|
|
1485
|
+
runId,
|
|
1486
|
+
taskId: current.taskId,
|
|
1487
|
+
timestamp: completedAt,
|
|
1488
|
+
agent: current.runtimeAdapter,
|
|
1489
|
+
summary: failureSummary
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
broadcastSnapshotInvalidation(state);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
const completedAt = new Date().toISOString();
|
|
1495
|
+
const failureSummary = error instanceof Error ? error.message : String(error);
|
|
1496
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1497
|
+
status: "failed",
|
|
1498
|
+
completedAt,
|
|
1499
|
+
errorText: failureSummary
|
|
1500
|
+
});
|
|
1501
|
+
if (run.taskId) {
|
|
1502
|
+
try {
|
|
1503
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...run, status: "failed", completedAt, errorText: failureSummary }, "failed", "Rig task run failed.", { errorText: failureSummary });
|
|
1504
|
+
} catch (sourceError) {
|
|
1505
|
+
const sourceFailure = lifecycleFailureMessage(run.taskId, "failed", sourceError);
|
|
1506
|
+
patchRunRecord(state.projectRoot, runId, { errorText: `${failureSummary}
|
|
1507
|
+
${sourceFailure}` });
|
|
1508
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
1509
|
+
id: `log:${runId}:task-source-failed-update`,
|
|
1510
|
+
title: "Task source failure update failed",
|
|
1511
|
+
detail: sourceFailure,
|
|
1512
|
+
tone: "error",
|
|
1513
|
+
status: "failed",
|
|
1514
|
+
createdAt: new Date().toISOString()
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
await enqueueRunLinearEvent(state.projectRoot, {
|
|
1519
|
+
type: "run.failed",
|
|
1520
|
+
runId,
|
|
1521
|
+
taskId: run.taskId,
|
|
1522
|
+
timestamp: completedAt,
|
|
1523
|
+
agent: run.runtimeAdapter,
|
|
1524
|
+
summary: failureSummary
|
|
1525
|
+
});
|
|
1526
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1527
|
+
id: `log:${runId}:error`,
|
|
1528
|
+
title: "Run failed",
|
|
1529
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1530
|
+
tone: "error",
|
|
1531
|
+
status: "failed",
|
|
1532
|
+
createdAt: new Date().toISOString()
|
|
1533
|
+
}, "local-run-failed");
|
|
1534
|
+
} finally {
|
|
1535
|
+
state.runProcesses.delete(runId);
|
|
1536
|
+
await reconcileScheduler(state, "local-run-terminal");
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
function resolveLocalRunCliProjectRoot(projectRoot) {
|
|
1542
|
+
const envCandidates = [
|
|
1543
|
+
process.env.RIG_HOST_PROJECT_ROOT?.trim(),
|
|
1544
|
+
process.env.PROJECT_RIG_ROOT?.trim()
|
|
1545
|
+
].filter((value) => !!value);
|
|
1546
|
+
for (const candidate of envCandidates) {
|
|
1547
|
+
if (existsSync6(resolve9(candidate, "packages/cli/bin/rig.ts"))) {
|
|
1548
|
+
return resolve9(candidate);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (existsSync6(resolve9(projectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1552
|
+
return projectRoot;
|
|
1553
|
+
}
|
|
1554
|
+
try {
|
|
1555
|
+
const monorepoRoot = resolveMonorepoRoot6(projectRoot);
|
|
1556
|
+
const outerProjectRoot = dirname5(dirname5(monorepoRoot));
|
|
1557
|
+
if (existsSync6(resolve9(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1558
|
+
return outerProjectRoot;
|
|
1559
|
+
}
|
|
1560
|
+
} catch {}
|
|
1561
|
+
return projectRoot;
|
|
1562
|
+
}
|
|
1563
|
+
async function resumeRunRecord(state, input) {
|
|
1564
|
+
const run = readAuthorityRun8(state.projectRoot, input.runId);
|
|
1565
|
+
if (!run) {
|
|
1566
|
+
throw new Error(`Run not found: ${input.runId}`);
|
|
1567
|
+
}
|
|
1568
|
+
if (run.mode !== "local") {
|
|
1569
|
+
throw new Error("Only local runs can be resumed.");
|
|
1570
|
+
}
|
|
1571
|
+
if (state.runProcesses.has(input.runId)) {
|
|
1572
|
+
throw new Error("Run is already active.");
|
|
1573
|
+
}
|
|
1574
|
+
if (run.status === "completed") {
|
|
1575
|
+
throw new Error("Completed runs cannot be resumed.");
|
|
1576
|
+
}
|
|
1577
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
|
|
1578
|
+
}
|
|
1579
|
+
function appendRunMessage(projectRoot, input) {
|
|
1580
|
+
const run = readAuthorityRun8(projectRoot, input.runId);
|
|
1581
|
+
if (!run) {
|
|
1582
|
+
throw new Error(`Run not found: ${input.runId}`);
|
|
1583
|
+
}
|
|
1584
|
+
const timelinePath = resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
|
|
1585
|
+
const existingLines = fileExists(timelinePath) ? readFileSync3(timelinePath, "utf8").trim() : "";
|
|
1586
|
+
const nextLine = JSON.stringify({
|
|
1587
|
+
id: input.messageId,
|
|
1588
|
+
type: "user_message",
|
|
1589
|
+
text: input.text,
|
|
1590
|
+
attachments: input.attachments ?? [],
|
|
1591
|
+
createdAt: input.createdAt
|
|
1592
|
+
});
|
|
1593
|
+
writeFileSync5(timelinePath, existingLines.length > 0 ? `${existingLines}
|
|
1594
|
+
${nextLine}
|
|
1595
|
+
` : `${nextLine}
|
|
1596
|
+
`, "utf8");
|
|
1597
|
+
writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
|
|
1598
|
+
...run,
|
|
1599
|
+
updatedAt: input.createdAt
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
async function stopRunRecord(stateOrProjectRoot, input) {
|
|
1603
|
+
const projectRoot = typeof stateOrProjectRoot === "string" ? stateOrProjectRoot : stateOrProjectRoot.projectRoot;
|
|
1604
|
+
const run = readAuthorityRun8(projectRoot, input.runId);
|
|
1605
|
+
if (!run) {
|
|
1606
|
+
throw new Error(`Run not found: ${input.runId}`);
|
|
1607
|
+
}
|
|
1608
|
+
if (typeof stateOrProjectRoot !== "string") {
|
|
1609
|
+
const processState = stateOrProjectRoot.runProcesses.get(input.runId);
|
|
1610
|
+
if (processState?.child) {
|
|
1611
|
+
processState.stopped = true;
|
|
1612
|
+
try {
|
|
1613
|
+
processState.child.kill("SIGTERM");
|
|
1614
|
+
} catch {}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
const nextRun = {
|
|
1618
|
+
...run,
|
|
1619
|
+
status: run.status === "completed" ? run.status : "stopped",
|
|
1620
|
+
completedAt: run.completedAt ?? input.createdAt,
|
|
1621
|
+
updatedAt: input.createdAt
|
|
1622
|
+
};
|
|
1623
|
+
writeJsonFile4(resolve9(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
|
|
1624
|
+
if (run.status !== "completed" && run.taskId) {
|
|
1625
|
+
const taskId = run.taskId;
|
|
1626
|
+
(async () => {
|
|
1627
|
+
try {
|
|
1628
|
+
await updateRunTaskSourceLifecycle(projectRoot, nextRun, "cancelled", "Rig task run was stopped before completion.");
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
const sourceFailure = lifecycleFailureMessage(taskId, "cancelled", error);
|
|
1631
|
+
patchRunRecord(projectRoot, input.runId, { errorText: sourceFailure });
|
|
1632
|
+
appendRunLogEntry(projectRoot, input.runId, {
|
|
1633
|
+
id: `log:${input.runId}:task-source-cancelled-update`,
|
|
1634
|
+
title: "Task source cancellation update failed",
|
|
1635
|
+
detail: sourceFailure,
|
|
1636
|
+
tone: "error",
|
|
1637
|
+
status: "failed",
|
|
1638
|
+
createdAt: new Date().toISOString()
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
})();
|
|
1642
|
+
}
|
|
1643
|
+
enqueueRunLinearEvent(projectRoot, {
|
|
1644
|
+
type: "run.interrupted",
|
|
1645
|
+
runId: input.runId,
|
|
1646
|
+
taskId: run.taskId,
|
|
1647
|
+
timestamp: input.createdAt,
|
|
1648
|
+
agent: run.runtimeAdapter,
|
|
1649
|
+
summary: "Run stopped before completion"
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
|
|
1653
|
+
const removedTaskIds = new Set(Array.from(taskIds).filter(Boolean));
|
|
1654
|
+
if (removedTaskIds.size === 0) {
|
|
1655
|
+
return readQueueState(projectRoot);
|
|
1656
|
+
}
|
|
1657
|
+
const next = readQueueState(projectRoot).filter((entry) => !removedTaskIds.has(entry.taskId)).map((entry, index) => ({ ...entry, position: index }));
|
|
1658
|
+
writeQueueState(projectRoot, next);
|
|
1659
|
+
return next;
|
|
1660
|
+
}
|
|
1661
|
+
var ORPHANABLE_LOCAL_RUN_STATUSES = new Set(["preparing", "running"]);
|
|
1662
|
+
function reconcileOrphanedLocalRuns(state, runs, nowIso) {
|
|
1663
|
+
let changed = false;
|
|
1664
|
+
for (const run of runs) {
|
|
1665
|
+
const status = normalizeString(run.status)?.toLowerCase() ?? "";
|
|
1666
|
+
const serverPid = run.serverPid;
|
|
1667
|
+
const wasStartedByRigServer = typeof serverPid === "number" || typeof serverPid === "string";
|
|
1668
|
+
if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
1672
|
+
patchRunRecord(state.projectRoot, run.runId, {
|
|
1673
|
+
status: "failed",
|
|
1674
|
+
completedAt: run.completedAt ?? nowIso,
|
|
1675
|
+
updatedAt: nowIso,
|
|
1676
|
+
errorText: detail
|
|
1677
|
+
});
|
|
1678
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
1679
|
+
id: `log:${run.runId}:stale-local-run`,
|
|
1680
|
+
title: "Run marked stale after server restart",
|
|
1681
|
+
detail,
|
|
1682
|
+
tone: "error",
|
|
1683
|
+
status: "failed",
|
|
1684
|
+
createdAt: nowIso
|
|
1685
|
+
});
|
|
1686
|
+
changed = true;
|
|
1687
|
+
}
|
|
1688
|
+
return changed;
|
|
1689
|
+
}
|
|
1690
|
+
async function reconcileScheduler(state, reason) {
|
|
1691
|
+
if (state.scheduler.reconciling) {
|
|
1692
|
+
state.scheduler.pending = true;
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
state.scheduler.reconciling = true;
|
|
1696
|
+
try {
|
|
1697
|
+
await withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
|
|
1698
|
+
do {
|
|
1699
|
+
state.scheduler.pending = false;
|
|
1700
|
+
const queue = readQueueState(state.projectRoot);
|
|
1701
|
+
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
1702
|
+
let runs = listAuthorityRuns7(state.projectRoot);
|
|
1703
|
+
let changed = reconcileOrphanedLocalRuns(state, runs, new Date().toISOString());
|
|
1704
|
+
if (changed) {
|
|
1705
|
+
runs = listAuthorityRuns7(state.projectRoot);
|
|
1706
|
+
}
|
|
1707
|
+
const plan = planSchedulerWork({
|
|
1708
|
+
queue,
|
|
1709
|
+
tasks: tasks.map((task) => ({
|
|
1710
|
+
id: task.id,
|
|
1711
|
+
status: task.status,
|
|
1712
|
+
priority: task.priority
|
|
1713
|
+
})),
|
|
1714
|
+
runs,
|
|
1715
|
+
localWorkerCount: resolveLocalSchedulerWorkerCount(),
|
|
1716
|
+
remoteWorkers: Array.from(state.remoteHosts.values()).map((host) => ({
|
|
1717
|
+
hostId: host.hostId,
|
|
1718
|
+
status: host.status,
|
|
1719
|
+
currentLeaseCount: host.currentLeaseCount,
|
|
1720
|
+
registeredAt: host.registeredAt,
|
|
1721
|
+
runtimeAdapters: host.runtimeAdapters
|
|
1722
|
+
}))
|
|
1723
|
+
});
|
|
1724
|
+
const claimedTaskIds = [
|
|
1725
|
+
...plan.pruneTaskIds,
|
|
1726
|
+
...plan.dispatches.map((dispatch) => dispatch.taskId)
|
|
1727
|
+
];
|
|
1728
|
+
if (claimedTaskIds.length > 0) {
|
|
1729
|
+
removeTaskIdsFromQueueState2(state.projectRoot, claimedTaskIds);
|
|
1730
|
+
}
|
|
1731
|
+
changed = changed || plan.pruneTaskIds.length > 0;
|
|
1732
|
+
for (const dispatch of plan.dispatches) {
|
|
1733
|
+
const task = tasks.find((entry) => entry.id === dispatch.taskId);
|
|
1734
|
+
if (!task) {
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
const createdAt = new Date().toISOString();
|
|
1738
|
+
const runId = crypto.randomUUID();
|
|
1739
|
+
const remoteHost = dispatch.workerKind === "remote" ? state.remoteHosts.get(dispatch.hostId) : null;
|
|
1740
|
+
const runtimeAdapter = dispatch.workerKind === "remote" ? normalizeRuntimeAdapter(remoteHost?.runtimeAdapters[0]) : normalizeRuntimeAdapter(process.env.RIG_RUNTIME_ADAPTER);
|
|
1741
|
+
await createRunRecord(state.projectRoot, {
|
|
1742
|
+
runId,
|
|
1743
|
+
workspaceId: RIG_WORKSPACE_ID,
|
|
1744
|
+
taskId: task.id,
|
|
1745
|
+
title: task.title,
|
|
1746
|
+
runtimeAdapter,
|
|
1747
|
+
executionTarget: dispatch.workerKind === "remote" ? "remote" : "local",
|
|
1748
|
+
remoteHostId: dispatch.workerKind === "remote" ? dispatch.hostId : undefined,
|
|
1749
|
+
createdAt
|
|
1750
|
+
}, () => state.snapshotService.getWorkspaceTasks());
|
|
1751
|
+
emitRigEvent(state, {
|
|
1752
|
+
type: "rig.task.run-created",
|
|
1753
|
+
aggregateId: task.id,
|
|
1754
|
+
payload: {
|
|
1755
|
+
runId,
|
|
1756
|
+
workspaceId: RIG_WORKSPACE_ID,
|
|
1757
|
+
taskId: task.id
|
|
1758
|
+
},
|
|
1759
|
+
createdAt
|
|
1760
|
+
});
|
|
1761
|
+
changed = true;
|
|
1762
|
+
if (dispatch.workerKind === "local") {
|
|
1763
|
+
await startLocalRun(state, runId);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (changed) {
|
|
1767
|
+
broadcastSnapshotInvalidation(state, `scheduler:${reason}`);
|
|
1768
|
+
}
|
|
1769
|
+
} while (state.scheduler.pending);
|
|
1770
|
+
});
|
|
1771
|
+
} finally {
|
|
1772
|
+
state.scheduler.reconciling = false;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
export {
|
|
1776
|
+
stopRunRecord,
|
|
1777
|
+
startLocalRun,
|
|
1778
|
+
resumeRunRecord,
|
|
1779
|
+
resolveLocalRunCliProjectRoot,
|
|
1780
|
+
removeTaskIdsFromQueueState2 as removeTaskIdsFromQueueState,
|
|
1781
|
+
reconcileScheduler,
|
|
1782
|
+
createRunRecord,
|
|
1783
|
+
appendRunMessage
|
|
1784
|
+
};
|