@h-rig/server 0.0.6-alpha.13 → 0.0.6-alpha.15
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/dist/src/index.js +699 -56
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/http-router.js +273 -25
- package/dist/src/server-helpers/run-io.js +13 -0
- package/dist/src/server-helpers/run-mutations.js +445 -32
- package/dist/src/server-helpers/run-writers.js +1 -2
- package/dist/src/server-helpers/ws-router.js +7 -1
- package/dist/src/server.js +697 -56
- package/package.json +4 -4
|
@@ -116,6 +116,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
116
116
|
running: "In Progress",
|
|
117
117
|
prOpen: "In Review",
|
|
118
118
|
ciFixing: "In Review",
|
|
119
|
+
merging: "Merging",
|
|
119
120
|
done: "Done",
|
|
120
121
|
needsAttention: "Needs Attention"
|
|
121
122
|
};
|
|
@@ -129,6 +130,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
129
130
|
return "prOpen";
|
|
130
131
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
131
132
|
return "ciFixing";
|
|
133
|
+
if (normalized === "merging" || normalized === "merge")
|
|
134
|
+
return "merging";
|
|
132
135
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
133
136
|
return "needsAttention";
|
|
134
137
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -224,6 +224,18 @@ function readJsonlFileTail(path, options) {
|
|
|
224
224
|
const completeLines = start > 0 ? lines.slice(1) : lines;
|
|
225
225
|
return parseJsonlRecords(completeLines.filter(Boolean).slice(-limit));
|
|
226
226
|
}
|
|
227
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
228
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
229
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
230
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
231
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
232
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
233
|
+
return {
|
|
234
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
235
|
+
nextCursor: String(endExclusive),
|
|
236
|
+
hasMore: endExclusive < entries.length
|
|
237
|
+
};
|
|
238
|
+
}
|
|
227
239
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
228
240
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
229
241
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -788,6 +800,11 @@ import {
|
|
|
788
800
|
buildTaskRunLifecycleComment,
|
|
789
801
|
updateConfiguredTaskSourceTask
|
|
790
802
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
803
|
+
import {
|
|
804
|
+
closeIssueAfterMergedPr,
|
|
805
|
+
commitRunChanges,
|
|
806
|
+
runPrAutomation
|
|
807
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
791
808
|
|
|
792
809
|
// packages/server/src/scheduler.ts
|
|
793
810
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -1027,6 +1044,9 @@ function asRecord(value) {
|
|
|
1027
1044
|
function asString(value) {
|
|
1028
1045
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
1029
1046
|
}
|
|
1047
|
+
function asNumber(value) {
|
|
1048
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1049
|
+
}
|
|
1030
1050
|
async function defaultGraphQLFetch(query, variables, token) {
|
|
1031
1051
|
const response = await fetch("https://api.github.com/graphql", {
|
|
1032
1052
|
method: "POST",
|
|
@@ -1043,6 +1063,32 @@ async function defaultGraphQLFetch(query, variables, token) {
|
|
|
1043
1063
|
}
|
|
1044
1064
|
return json.data;
|
|
1045
1065
|
}
|
|
1066
|
+
function projectNodesFrom(data) {
|
|
1067
|
+
const root = asRecord(data);
|
|
1068
|
+
const owner = asRecord(root?.organization) ?? asRecord(root?.user);
|
|
1069
|
+
const projects = asRecord(owner?.projectsV2);
|
|
1070
|
+
const nodes = projects?.nodes;
|
|
1071
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
1072
|
+
}
|
|
1073
|
+
async function listGitHubProjects(input) {
|
|
1074
|
+
const query = `
|
|
1075
|
+
query RigListProjects($owner: String!, $first: Int!) {
|
|
1076
|
+
organization(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1077
|
+
user(login: $owner) { projectsV2(first: $first, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { id number title url } } }
|
|
1078
|
+
}
|
|
1079
|
+
`;
|
|
1080
|
+
const fetchGraphQL = input.fetchGraphQL ?? defaultGraphQLFetch;
|
|
1081
|
+
const data = await fetchGraphQL(query, { owner: input.owner, first: input.first ?? 20 }, input.token);
|
|
1082
|
+
return projectNodesFrom(data).flatMap((node) => {
|
|
1083
|
+
const record = asRecord(node);
|
|
1084
|
+
const id = asString(record?.id);
|
|
1085
|
+
const number = asNumber(record?.number);
|
|
1086
|
+
const title = asString(record?.title);
|
|
1087
|
+
if (!id || number === undefined || !title)
|
|
1088
|
+
return [];
|
|
1089
|
+
return [{ id, number, title, ...asString(record?.url) ? { url: asString(record?.url) } : {} }];
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1046
1092
|
async function resolveProjectStatusField(input) {
|
|
1047
1093
|
const query = `
|
|
1048
1094
|
query RigProjectStatusField($projectId: ID!) {
|
|
@@ -1137,6 +1183,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
1137
1183
|
running: "In Progress",
|
|
1138
1184
|
prOpen: "In Review",
|
|
1139
1185
|
ciFixing: "In Review",
|
|
1186
|
+
merging: "Merging",
|
|
1140
1187
|
done: "Done",
|
|
1141
1188
|
needsAttention: "Needs Attention"
|
|
1142
1189
|
};
|
|
@@ -1150,6 +1197,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
1150
1197
|
return "prOpen";
|
|
1151
1198
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
1152
1199
|
return "ciFixing";
|
|
1200
|
+
if (normalized === "merging" || normalized === "merge")
|
|
1201
|
+
return "merging";
|
|
1153
1202
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
1154
1203
|
return "needsAttention";
|
|
1155
1204
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -1225,7 +1274,8 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
1225
1274
|
"needs-attention",
|
|
1226
1275
|
"stopped"
|
|
1227
1276
|
]);
|
|
1228
|
-
var
|
|
1277
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
1278
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
1229
1279
|
|
|
1230
1280
|
// packages/server/src/server-helpers/ws-router.ts
|
|
1231
1281
|
import {
|
|
@@ -2000,6 +2050,75 @@ function buildProjectConfigStatus(root) {
|
|
|
2000
2050
|
suggestion: kind === "missing" ? "Run `rig init` in the project root to scaffold rig.config.ts." : null
|
|
2001
2051
|
};
|
|
2002
2052
|
}
|
|
2053
|
+
var RIG_GITHUB_LIFECYCLE_LABELS = [
|
|
2054
|
+
"ready",
|
|
2055
|
+
"blocked",
|
|
2056
|
+
"in-progress",
|
|
2057
|
+
"under-review",
|
|
2058
|
+
"failed",
|
|
2059
|
+
"cancelled",
|
|
2060
|
+
"rig:running",
|
|
2061
|
+
"rig:pr-open",
|
|
2062
|
+
"rig:ci-fixing",
|
|
2063
|
+
"rig:merging",
|
|
2064
|
+
"rig:done",
|
|
2065
|
+
"rig:needs-attention"
|
|
2066
|
+
];
|
|
2067
|
+
function githubProjectsEnabled(config) {
|
|
2068
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2069
|
+
return false;
|
|
2070
|
+
const root = config;
|
|
2071
|
+
const github = root.github && typeof root.github === "object" && !Array.isArray(root.github) ? root.github : null;
|
|
2072
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
2073
|
+
return projects?.enabled === true;
|
|
2074
|
+
}
|
|
2075
|
+
function githubIssueSourceRepo(config) {
|
|
2076
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
2077
|
+
return null;
|
|
2078
|
+
const root = config;
|
|
2079
|
+
const taskSource = root.taskSource && typeof root.taskSource === "object" && !Array.isArray(root.taskSource) ? root.taskSource : null;
|
|
2080
|
+
const owner = normalizeString(taskSource?.owner);
|
|
2081
|
+
const repo = normalizeString(taskSource?.repo);
|
|
2082
|
+
if (taskSource?.kind === "github-issues" && owner && repo)
|
|
2083
|
+
return { owner, repo };
|
|
2084
|
+
const project = root.project && typeof root.project === "object" && !Array.isArray(root.project) ? root.project : null;
|
|
2085
|
+
const slug = normalizeString(project?.repo) ?? normalizeString(project?.name);
|
|
2086
|
+
const match = slug?.match(/^([^/]+)\/([^/]+)$/);
|
|
2087
|
+
return match ? { owner: match[1], repo: match[2] } : null;
|
|
2088
|
+
}
|
|
2089
|
+
async function ensureGitHubLifecycleLabels(projectRoot, config) {
|
|
2090
|
+
const repo = githubIssueSourceRepo(config);
|
|
2091
|
+
if (!repo)
|
|
2092
|
+
return { ok: false, ready: false, labelsReady: false, reason: "not-github-issues-source", labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2093
|
+
const token = createGitHubAuthStore(projectRoot).readToken();
|
|
2094
|
+
if (!token)
|
|
2095
|
+
return { ok: false, ready: false, labelsReady: false, reason: "missing-token", repo, labels: RIG_GITHUB_LIFECYCLE_LABELS };
|
|
2096
|
+
const existingResponse = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels?per_page=100`, {
|
|
2097
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "rig-server" }
|
|
2098
|
+
});
|
|
2099
|
+
const existingJson = await existingResponse.json().catch(() => []);
|
|
2100
|
+
const existing = new Set(Array.isArray(existingJson) ? existingJson.flatMap((entry) => entry && typeof entry === "object" && typeof entry.name === "string" ? [entry.name] : []) : []);
|
|
2101
|
+
const created = [];
|
|
2102
|
+
const alreadyPresent = [];
|
|
2103
|
+
const failed = [];
|
|
2104
|
+
for (const label of RIG_GITHUB_LIFECYCLE_LABELS) {
|
|
2105
|
+
if (existing.has(label)) {
|
|
2106
|
+
alreadyPresent.push(label);
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const response = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/labels`, {
|
|
2110
|
+
method: "POST",
|
|
2111
|
+
headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "rig-server" },
|
|
2112
|
+
body: JSON.stringify({ name: label, color: label.startsWith("rig:") ? "6f42c1" : "ededed", description: label.startsWith("rig:") ? "Task status managed by Rig" : "Task lifecycle status managed by Rig" })
|
|
2113
|
+
});
|
|
2114
|
+
if (response.ok || response.status === 422) {
|
|
2115
|
+
(response.status === 422 ? alreadyPresent : created).push(label);
|
|
2116
|
+
} else {
|
|
2117
|
+
failed.push({ label, error: await response.text().catch(() => response.statusText) });
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return { ok: failed.length === 0, ready: failed.length === 0, labelsReady: failed.length === 0, repo, labels: RIG_GITHUB_LIFECYCLE_LABELS, created, existing: alreadyPresent, failed };
|
|
2121
|
+
}
|
|
2003
2122
|
function normalizeCommit(value) {
|
|
2004
2123
|
const raw = normalizeString(value);
|
|
2005
2124
|
return raw && /^[0-9a-f]{7,40}$/i.test(raw) ? raw : null;
|
|
@@ -2398,15 +2517,33 @@ say "Installing @h-rig/cli@latest"
|
|
|
2398
2517
|
bun add -g @h-rig/cli@latest
|
|
2399
2518
|
|
|
2400
2519
|
export BUN_INSTALL="\${BUN_INSTALL:-$HOME/.bun}"
|
|
2401
|
-
|
|
2520
|
+
BUN_RIG="$BUN_INSTALL/bin/rig"
|
|
2521
|
+
if [ ! -x "$BUN_RIG" ]; then
|
|
2522
|
+
printf 'rig-install: expected Bun global rig at %s but it was not executable.
|
|
2523
|
+
' "$BUN_RIG" >&2
|
|
2524
|
+
exit 1
|
|
2525
|
+
fi
|
|
2526
|
+
|
|
2527
|
+
USER_BIN="$HOME/.local/bin"
|
|
2528
|
+
mkdir -p "$USER_BIN"
|
|
2529
|
+
cat > "$USER_BIN/rig" <<'RIG_SHIM'
|
|
2530
|
+
#!/usr/bin/env bash
|
|
2531
|
+
set -euo pipefail
|
|
2532
|
+
exec "\${BUN_INSTALL:-$HOME/.bun}/bin/rig" "$@"
|
|
2533
|
+
RIG_SHIM
|
|
2534
|
+
chmod +x "$USER_BIN/rig"
|
|
2535
|
+
|
|
2536
|
+
export PATH="$USER_BIN:$BUN_INSTALL/bin:$PATH"
|
|
2537
|
+
if command -v hash >/dev/null 2>&1; then hash -r; fi
|
|
2402
2538
|
|
|
2403
2539
|
if ! command -v rig >/dev/null 2>&1; then
|
|
2404
|
-
printf 'rig-install: rig installed, but rig is not on PATH. Add %s/bin to PATH and retry.
|
|
2405
|
-
' "$BUN_INSTALL" >&2
|
|
2540
|
+
printf 'rig-install: rig installed, but rig is not on PATH. Add %s and %s/bin to PATH and retry.
|
|
2541
|
+
' "$USER_BIN" "$BUN_INSTALL" >&2
|
|
2406
2542
|
exit 1
|
|
2407
2543
|
fi
|
|
2408
2544
|
|
|
2409
2545
|
say "Verifying rig"
|
|
2546
|
+
"$BUN_RIG" --help >/dev/null
|
|
2410
2547
|
rig --help >/dev/null
|
|
2411
2548
|
say "Done. Run: rig --help"
|
|
2412
2549
|
`;
|
|
@@ -3145,16 +3282,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3145
3282
|
if (!source) {
|
|
3146
3283
|
return deps.badRequest("No task source is configured");
|
|
3147
3284
|
}
|
|
3285
|
+
if (!source.updateTask && !(update.status && source.updateStatus)) {
|
|
3286
|
+
return deps.badRequest("Configured task source does not support updates");
|
|
3287
|
+
}
|
|
3148
3288
|
const taskBeforeUpdate = source.get ? await source.get(id).catch(() => {
|
|
3149
3289
|
return;
|
|
3150
3290
|
}) : (await deps.snapshotService.getWorkspaceTasks().catch(() => [])).find((task) => task.id === id);
|
|
3151
|
-
if (source.updateTask) {
|
|
3152
|
-
await source.updateTask(id, update);
|
|
3153
|
-
} else if (update.status && source.updateStatus) {
|
|
3154
|
-
await source.updateStatus(id, update.status);
|
|
3155
|
-
} else {
|
|
3156
|
-
return deps.badRequest("Configured task source does not support updates");
|
|
3157
|
-
}
|
|
3158
3291
|
const issueNodeId = normalizeString(body.issueNodeId) ?? extractGitHubIssueNodeId(taskBeforeUpdate);
|
|
3159
3292
|
const projectSync = update.status ? await syncGitHubProjectStatusForTaskUpdate({
|
|
3160
3293
|
taskId: id,
|
|
@@ -3163,6 +3296,35 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3163
3296
|
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
3164
3297
|
config: ctx?.config
|
|
3165
3298
|
}).catch((error) => ({ synced: false, reason: `error:${error instanceof Error ? error.message : String(error)}` })) : { synced: false, reason: "missing-status" };
|
|
3299
|
+
if (update.status && githubProjectsEnabled(ctx?.config) && projectSync.synced === false) {
|
|
3300
|
+
return deps.jsonResponse({ ok: false, id, projectSync, error: `GitHub Project status sync failed: ${String(projectSync.reason)}` }, 502);
|
|
3301
|
+
}
|
|
3302
|
+
try {
|
|
3303
|
+
if (source.updateTask) {
|
|
3304
|
+
await source.updateTask(id, update);
|
|
3305
|
+
} else if (update.status && source.updateStatus) {
|
|
3306
|
+
await source.updateStatus(id, update.status);
|
|
3307
|
+
}
|
|
3308
|
+
} catch (error) {
|
|
3309
|
+
let rollback = null;
|
|
3310
|
+
const previousStatus = normalizeString(taskBeforeUpdate?.status) ?? normalizeString(taskBeforeUpdate?.sourceStatus);
|
|
3311
|
+
if (update.status && previousStatus && githubProjectsEnabled(ctx?.config) && projectSync.synced !== false) {
|
|
3312
|
+
rollback = await syncGitHubProjectStatusForTaskUpdate({
|
|
3313
|
+
taskId: id,
|
|
3314
|
+
status: previousStatus,
|
|
3315
|
+
issueNodeId,
|
|
3316
|
+
token: createGitHubAuthStore(state.projectRoot).readToken(),
|
|
3317
|
+
config: ctx?.config
|
|
3318
|
+
}).catch((rollbackError) => ({ synced: false, reason: `rollback-error:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}` }));
|
|
3319
|
+
}
|
|
3320
|
+
return deps.jsonResponse({
|
|
3321
|
+
ok: false,
|
|
3322
|
+
id,
|
|
3323
|
+
projectSync,
|
|
3324
|
+
rollback,
|
|
3325
|
+
error: `Task source update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3326
|
+
}, 502);
|
|
3327
|
+
}
|
|
3166
3328
|
deps.snapshotService.invalidate("github-issue-updated");
|
|
3167
3329
|
await state.taskProjectionReconciler?.tick("github-issue-updated").catch(() => {
|
|
3168
3330
|
return;
|
|
@@ -3171,25 +3333,40 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3171
3333
|
return deps.jsonResponse({ ok: true, id, projectSync });
|
|
3172
3334
|
}
|
|
3173
3335
|
if (url.pathname === "/api/workspace/task-labels") {
|
|
3336
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot).catch(() => null);
|
|
3337
|
+
if (url.searchParams.get("ensure") === "1" || req.method === "POST") {
|
|
3338
|
+
return deps.jsonResponse(await ensureGitHubLifecycleLabels(state.projectRoot, ctx?.config));
|
|
3339
|
+
}
|
|
3174
3340
|
return deps.jsonResponse({
|
|
3175
3341
|
ok: true,
|
|
3176
3342
|
ready: true,
|
|
3177
3343
|
labelsReady: true,
|
|
3178
|
-
labels: [
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
3344
|
+
labels: [...RIG_GITHUB_LIFECYCLE_LABELS],
|
|
3345
|
+
note: "Lifecycle labels are required during init; call POST /api/workspace/task-labels or ?ensure=1 to proactively create them."
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
if (url.pathname === "/api/github/projects" && req.method === "GET") {
|
|
3349
|
+
const owner = normalizeString(url.searchParams.get("owner"));
|
|
3350
|
+
if (!owner)
|
|
3351
|
+
return deps.badRequest("owner is required");
|
|
3352
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3353
|
+
if (!token)
|
|
3354
|
+
return deps.jsonResponse({ ok: false, error: "missing-token", projects: [] }, 401);
|
|
3355
|
+
const projects = await listGitHubProjects({ owner, token }).catch((error) => {
|
|
3356
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
3192
3357
|
});
|
|
3358
|
+
return deps.jsonResponse({ ok: true, projects });
|
|
3359
|
+
}
|
|
3360
|
+
const projectStatusMatch = url.pathname.match(/^\/api\/github\/projects\/([^/]+)\/status-field$/);
|
|
3361
|
+
if (projectStatusMatch && req.method === "GET") {
|
|
3362
|
+
const projectId = decodeURIComponent(projectStatusMatch[1]);
|
|
3363
|
+
const token = createGitHubAuthStore(state.projectRoot).readToken();
|
|
3364
|
+
if (!token)
|
|
3365
|
+
return deps.jsonResponse({ ok: false, error: "missing-token" }, 401);
|
|
3366
|
+
const field = await resolveProjectStatusField({ projectId, token }).catch((error) => {
|
|
3367
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
3368
|
+
});
|
|
3369
|
+
return deps.jsonResponse({ ok: true, field });
|
|
3193
3370
|
}
|
|
3194
3371
|
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
3195
3372
|
const body = await deps.readJsonBody(req);
|
|
@@ -4355,6 +4532,69 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4355
4532
|
}
|
|
4356
4533
|
const run = leaseValidation.run;
|
|
4357
4534
|
const completedAt = new Date().toISOString();
|
|
4535
|
+
const workspaceDir = normalizeString(body.workspaceDir) ?? normalizeString(body.runtimeWorkspace) ?? normalizeString(run.worktreePath);
|
|
4536
|
+
if (run.taskId && workspaceDir) {
|
|
4537
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4538
|
+
status: "reviewing",
|
|
4539
|
+
completedAt: null,
|
|
4540
|
+
hostId,
|
|
4541
|
+
endpointId: leaseId,
|
|
4542
|
+
worktreePath: workspaceDir,
|
|
4543
|
+
serverCloseout: {
|
|
4544
|
+
status: "pending",
|
|
4545
|
+
phase: "queued",
|
|
4546
|
+
requestedAt: completedAt,
|
|
4547
|
+
updatedAt: completedAt,
|
|
4548
|
+
runtimeWorkspace: workspaceDir,
|
|
4549
|
+
branch: normalizeString(body.branch) ?? normalizeString(run.branch) ?? `rig/${run.taskId}-${runId}`,
|
|
4550
|
+
taskId: run.taskId,
|
|
4551
|
+
source: "remote-complete"
|
|
4552
|
+
}
|
|
4553
|
+
});
|
|
4554
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4555
|
+
id: `log:${runId}:remote-server-closeout-requested`,
|
|
4556
|
+
title: "Server-owned closeout requested",
|
|
4557
|
+
detail: "Remote run completed provider work and handed commit/PR/review/merge closeout to the Rig server.",
|
|
4558
|
+
tone: "info",
|
|
4559
|
+
status: "reviewing",
|
|
4560
|
+
createdAt: completedAt,
|
|
4561
|
+
payload: { workspaceDir, hostId, leaseId }
|
|
4562
|
+
}, "remote-server-closeout-requested");
|
|
4563
|
+
deps.runServerOwnedPrCloseout(state, runId).catch((error) => {
|
|
4564
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4565
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
4566
|
+
status: "failed",
|
|
4567
|
+
completedAt: new Date().toISOString(),
|
|
4568
|
+
errorText: detail,
|
|
4569
|
+
serverCloseout: {
|
|
4570
|
+
status: "failed",
|
|
4571
|
+
phase: "failed",
|
|
4572
|
+
updatedAt: new Date().toISOString(),
|
|
4573
|
+
error: detail
|
|
4574
|
+
}
|
|
4575
|
+
});
|
|
4576
|
+
deps.appendRunLogEntryAndBroadcast(state, runId, {
|
|
4577
|
+
id: `log:${runId}:remote-server-closeout-failed`,
|
|
4578
|
+
title: "Server-owned closeout failed",
|
|
4579
|
+
detail,
|
|
4580
|
+
tone: "error",
|
|
4581
|
+
status: "failed",
|
|
4582
|
+
createdAt: new Date().toISOString()
|
|
4583
|
+
}, "remote-server-closeout-failed");
|
|
4584
|
+
}).finally(() => {
|
|
4585
|
+
deps.reconcileScheduler(state, "remote-server-closeout-terminal");
|
|
4586
|
+
});
|
|
4587
|
+
deps.broadcastSnapshotInvalidation(state);
|
|
4588
|
+
return deps.jsonResponse({
|
|
4589
|
+
ok: true,
|
|
4590
|
+
workspaceId: normalizeString(body.workspaceId) ?? RIG_WORKSPACE_ID,
|
|
4591
|
+
hostId,
|
|
4592
|
+
runId,
|
|
4593
|
+
leaseId,
|
|
4594
|
+
closeout: "server-owned",
|
|
4595
|
+
acceptedAt: new Date().toISOString()
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4358
4598
|
patchRunRecord(state.projectRoot, runId, {
|
|
4359
4599
|
status: "completed",
|
|
4360
4600
|
completedAt,
|
|
@@ -4497,6 +4737,14 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
4497
4737
|
const page = await readRunLogsPage(state.projectRoot, runId, { limit, cursor });
|
|
4498
4738
|
return deps.jsonResponse(page);
|
|
4499
4739
|
}
|
|
4740
|
+
const runTimelineMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/timeline$/);
|
|
4741
|
+
if (runTimelineMatch) {
|
|
4742
|
+
const runId = decodeURIComponent(runTimelineMatch[1]);
|
|
4743
|
+
const limit = Number.parseInt(url.searchParams.get("limit") || "500", 10);
|
|
4744
|
+
const cursor = normalizeString(url.searchParams.get("cursor"));
|
|
4745
|
+
const page = await readRunTimelinePage(state.projectRoot, runId, { limit, cursor });
|
|
4746
|
+
return deps.jsonResponse(page);
|
|
4747
|
+
}
|
|
4500
4748
|
const runSteerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
|
|
4501
4749
|
if (runSteerMatch && req.method === "POST") {
|
|
4502
4750
|
const runId = decodeURIComponent(runSteerMatch[1]);
|
|
@@ -167,6 +167,18 @@ function readJsonlFileTail(path, options) {
|
|
|
167
167
|
function readRawRunLogs(projectRoot, runId) {
|
|
168
168
|
return readJsonlFile(runLogsPath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object"));
|
|
169
169
|
}
|
|
170
|
+
async function readRunTimelinePage(projectRoot, runId, options = {}) {
|
|
171
|
+
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
172
|
+
const cursor = options.cursor == null ? 0 : Number.parseInt(options.cursor, 10);
|
|
173
|
+
const entries = readJsonlFile(runTimelinePath(projectRoot, runId)).filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
174
|
+
const startInclusive = Number.isFinite(cursor) ? Math.max(0, Math.min(cursor, entries.length)) : 0;
|
|
175
|
+
const endExclusive = Math.min(entries.length, startInclusive + limit);
|
|
176
|
+
return {
|
|
177
|
+
entries: entries.slice(startInclusive, endExclusive).map((entry, offset) => ({ ...entry, cursor: startInclusive + offset + 1 })),
|
|
178
|
+
nextCursor: String(endExclusive),
|
|
179
|
+
hasMore: endExclusive < entries.length
|
|
180
|
+
};
|
|
181
|
+
}
|
|
170
182
|
var INITIAL_RUN_LOG_TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
171
183
|
async function readRunLogsPage(projectRoot, runId, options = {}) {
|
|
172
184
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 200), 500));
|
|
@@ -252,6 +264,7 @@ export {
|
|
|
252
264
|
remoteArtifactsRoot,
|
|
253
265
|
readUserInputsForRuns,
|
|
254
266
|
readUserInputs,
|
|
267
|
+
readRunTimelinePage,
|
|
255
268
|
readRunLogsPage,
|
|
256
269
|
readRunDetails,
|
|
257
270
|
readRawRunLogs,
|