@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.20
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 +23 -0
- package/dist/src/index.js +1513 -331
- package/dist/src/server-helpers/github-api-session-index.js +107 -0
- package/dist/src/server-helpers/github-auth-store.js +68 -24
- package/dist/src/server-helpers/github-project-status-sync.js +3 -0
- package/dist/src/server-helpers/github-user-namespace.js +102 -0
- package/dist/src/server-helpers/http-router.js +1207 -185
- package/dist/src/server-helpers/issue-analysis.js +30 -11
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-io.js +13 -0
- package/dist/src/server-helpers/run-mutations.js +599 -114
- 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 +1512 -331
- package/package.json +4 -4
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// packages/server/src/server-helpers/run-mutations.ts
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { loadConfig } from "@rig/core/load-config";
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { dirname as
|
|
5
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
6
|
+
import { dirname as dirname6, relative as relative2, resolve as resolve10 } from "path";
|
|
7
7
|
import {
|
|
8
8
|
listAuthorityRuns as listAuthorityRuns7,
|
|
9
9
|
readAuthorityRun as readAuthorityRun8,
|
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
buildTaskRunLifecycleComment as buildTaskRunLifecycleComment2,
|
|
17
17
|
updateConfiguredTaskSourceTask as updateConfiguredTaskSourceTask2
|
|
18
18
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
19
|
+
import {
|
|
20
|
+
closeIssueAfterMergedPr,
|
|
21
|
+
commitRunChanges,
|
|
22
|
+
runPrAutomation
|
|
23
|
+
} from "@rig/runtime/control-plane/native/pr-automation";
|
|
19
24
|
|
|
20
25
|
// packages/server/src/scheduler.ts
|
|
21
26
|
import { normalizeTaskLifecycleStatus } from "@rig/runtime/control-plane/state-sync/types";
|
|
@@ -98,8 +103,8 @@ function normalizeStatus(value) {
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
// packages/server/src/server.ts
|
|
101
|
-
import { existsSync as
|
|
102
|
-
import { dirname as
|
|
106
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
107
|
+
import { dirname as dirname5, resolve as resolve8 } from "path";
|
|
103
108
|
import {
|
|
104
109
|
listAuthorityArtifactRoots,
|
|
105
110
|
listAuthorityRuns as listAuthorityRuns6,
|
|
@@ -180,6 +185,9 @@ import {
|
|
|
180
185
|
readJsonlFile,
|
|
181
186
|
resolveAuthorityRunDir
|
|
182
187
|
} from "@rig/runtime/control-plane/authority-files";
|
|
188
|
+
function runTimelinePath(projectRoot, runId) {
|
|
189
|
+
return resolve2(resolveAuthorityRunDir(projectRoot, runId), "timeline.jsonl");
|
|
190
|
+
}
|
|
183
191
|
function runLogsPath(projectRoot, runId) {
|
|
184
192
|
return resolve2(resolveAuthorityRunDir(projectRoot, runId), "logs.jsonl");
|
|
185
193
|
}
|
|
@@ -295,6 +303,24 @@ var snapshotCache = new Map;
|
|
|
295
303
|
var contextCache = new Map;
|
|
296
304
|
var taskListCache = new Map;
|
|
297
305
|
|
|
306
|
+
// packages/server/src/server-helpers/task-projection.ts
|
|
307
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync, writeFileSync as writeFileSync3 } from "fs";
|
|
308
|
+
import { resolve as resolve5 } from "path";
|
|
309
|
+
function projectionPath(projectRoot) {
|
|
310
|
+
return resolve5(projectRoot, ".rig", "state", "task-projection.json");
|
|
311
|
+
}
|
|
312
|
+
function readTaskProjection(projectRoot) {
|
|
313
|
+
const file = projectionPath(projectRoot);
|
|
314
|
+
if (!existsSync3(file))
|
|
315
|
+
return null;
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
318
|
+
return parsed && parsed.version === 1 && Array.isArray(parsed.tasks) ? parsed : null;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
298
324
|
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
299
325
|
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
300
326
|
|
|
@@ -302,13 +328,20 @@ import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
|
302
328
|
import { RIG_WS_CHANNELS } from "@rig/contracts";
|
|
303
329
|
|
|
304
330
|
// packages/server/src/server-helpers/run-writers.ts
|
|
305
|
-
import { resolve as
|
|
331
|
+
import { resolve as resolve6 } from "path";
|
|
306
332
|
import {
|
|
307
333
|
appendJsonlRecord,
|
|
308
334
|
readAuthorityRun as readAuthorityRun3,
|
|
309
335
|
resolveAuthorityRunDir as resolveAuthorityRunDir2,
|
|
310
336
|
writeJsonFile as writeJsonFile2
|
|
311
337
|
} from "@rig/runtime/control-plane/authority-files";
|
|
338
|
+
function appendRunTimelineEntry(projectRoot, runId, value) {
|
|
339
|
+
if (!readAuthorityRun3(projectRoot, runId)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
appendJsonlRecord(runTimelinePath(projectRoot, runId), value);
|
|
343
|
+
patchRunRecord(projectRoot, runId, {});
|
|
344
|
+
}
|
|
312
345
|
function appendRunLogEntry(projectRoot, runId, value) {
|
|
313
346
|
if (!readAuthorityRun3(projectRoot, runId)) {
|
|
314
347
|
return;
|
|
@@ -327,7 +360,7 @@ function patchRunRecord(projectRoot, runId, patch) {
|
|
|
327
360
|
...patch,
|
|
328
361
|
updatedAt: normalizeString(patch.updatedAt) ?? new Date().toISOString()
|
|
329
362
|
};
|
|
330
|
-
writeJsonFile2(
|
|
363
|
+
writeJsonFile2(resolve6(resolveAuthorityRunDir2(projectRoot, runId), "run.json"), next);
|
|
331
364
|
return next;
|
|
332
365
|
}
|
|
333
366
|
function buildRunStartPatch(startedAt) {
|
|
@@ -335,8 +368,7 @@ function buildRunStartPatch(startedAt) {
|
|
|
335
368
|
status: "preparing",
|
|
336
369
|
startedAt,
|
|
337
370
|
completedAt: null,
|
|
338
|
-
errorText: null
|
|
339
|
-
serverPid: process.pid
|
|
371
|
+
errorText: null
|
|
340
372
|
};
|
|
341
373
|
}
|
|
342
374
|
|
|
@@ -460,8 +492,8 @@ import {
|
|
|
460
492
|
|
|
461
493
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
462
494
|
import { randomBytes } from "crypto";
|
|
463
|
-
import { chmodSync, existsSync as
|
|
464
|
-
import { resolve as
|
|
495
|
+
import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
496
|
+
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
465
497
|
function cleanString(value) {
|
|
466
498
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
467
499
|
}
|
|
@@ -491,11 +523,31 @@ function parseApiSessions(value) {
|
|
|
491
523
|
}];
|
|
492
524
|
});
|
|
493
525
|
}
|
|
526
|
+
function parsePendingDevice(value) {
|
|
527
|
+
if (!value || typeof value !== "object")
|
|
528
|
+
return null;
|
|
529
|
+
const record = value;
|
|
530
|
+
const pollId = cleanString(record.pollId);
|
|
531
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
532
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
533
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
534
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
535
|
+
return null;
|
|
536
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
537
|
+
}
|
|
538
|
+
function parsePendingDevices(value) {
|
|
539
|
+
if (!Array.isArray(value))
|
|
540
|
+
return [];
|
|
541
|
+
return value.flatMap((entry) => {
|
|
542
|
+
const pending = parsePendingDevice(entry);
|
|
543
|
+
return pending ? [pending] : [];
|
|
544
|
+
});
|
|
545
|
+
}
|
|
494
546
|
function readStoredAuth(stateFile) {
|
|
495
|
-
if (!
|
|
547
|
+
if (!existsSync4(stateFile))
|
|
496
548
|
return {};
|
|
497
549
|
try {
|
|
498
|
-
const parsed = JSON.parse(
|
|
550
|
+
const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
|
|
499
551
|
return {
|
|
500
552
|
...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
|
|
501
553
|
login: cleanString(parsed.login),
|
|
@@ -504,6 +556,7 @@ function readStoredAuth(stateFile) {
|
|
|
504
556
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
505
557
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
506
558
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
559
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
507
560
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
508
561
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
509
562
|
};
|
|
@@ -511,34 +564,36 @@ function readStoredAuth(stateFile) {
|
|
|
511
564
|
return {};
|
|
512
565
|
}
|
|
513
566
|
}
|
|
514
|
-
function parsePendingDevice(value) {
|
|
515
|
-
if (!value || typeof value !== "object")
|
|
516
|
-
return null;
|
|
517
|
-
const record = value;
|
|
518
|
-
const pollId = cleanString(record.pollId);
|
|
519
|
-
const deviceCode = cleanString(record.deviceCode);
|
|
520
|
-
const expiresAt = cleanString(record.expiresAt);
|
|
521
|
-
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
522
|
-
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
523
|
-
return null;
|
|
524
|
-
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
525
|
-
}
|
|
526
567
|
function newApiSessionToken() {
|
|
527
568
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
528
569
|
}
|
|
529
570
|
function writeStoredAuth(stateFile, payload) {
|
|
530
|
-
|
|
531
|
-
|
|
571
|
+
mkdirSync4(dirname4(stateFile), { recursive: true });
|
|
572
|
+
writeFileSync4(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
532
573
|
`, { encoding: "utf8", mode: 384 });
|
|
533
574
|
try {
|
|
534
575
|
chmodSync(stateFile, 384);
|
|
535
576
|
} catch {}
|
|
536
577
|
}
|
|
578
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
579
|
+
return resolve7(projectRoot, ".rig", "state", "github-auth.json");
|
|
580
|
+
}
|
|
537
581
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
538
|
-
return
|
|
582
|
+
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
539
583
|
}
|
|
540
|
-
function
|
|
541
|
-
const
|
|
584
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
585
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
586
|
+
mkdirSync4(dirname4(targetFile), { recursive: true });
|
|
587
|
+
if (existsSync4(stateFile)) {
|
|
588
|
+
copyFileSync(stateFile, targetFile);
|
|
589
|
+
try {
|
|
590
|
+
chmodSync(targetFile, 384);
|
|
591
|
+
} catch {}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
writeStoredAuth(targetFile, {});
|
|
595
|
+
}
|
|
596
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
542
597
|
return {
|
|
543
598
|
stateFile,
|
|
544
599
|
status(options) {
|
|
@@ -568,6 +623,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
568
623
|
scopes: input.scopes ?? [],
|
|
569
624
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
570
625
|
pendingDevice: null,
|
|
626
|
+
pendingDevices: [],
|
|
571
627
|
apiSessions: previous.apiSessions ?? [],
|
|
572
628
|
updatedAt: new Date().toISOString()
|
|
573
629
|
});
|
|
@@ -596,15 +652,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
596
652
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
597
653
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
598
654
|
},
|
|
599
|
-
copyToProjectRoot(
|
|
600
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
655
|
+
copyToProjectRoot(projectRoot) {
|
|
656
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
601
657
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
602
658
|
},
|
|
659
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
660
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
661
|
+
},
|
|
603
662
|
savePendingDevice(input) {
|
|
604
663
|
const previous = readStoredAuth(stateFile);
|
|
664
|
+
const pendingDevices = [
|
|
665
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
666
|
+
...previous.pendingDevices ?? [],
|
|
667
|
+
input
|
|
668
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
605
669
|
writeStoredAuth(stateFile, {
|
|
606
670
|
...previous,
|
|
607
|
-
pendingDevice:
|
|
671
|
+
pendingDevice: null,
|
|
672
|
+
pendingDevices,
|
|
608
673
|
updatedAt: new Date().toISOString()
|
|
609
674
|
});
|
|
610
675
|
},
|
|
@@ -617,23 +682,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
617
682
|
});
|
|
618
683
|
},
|
|
619
684
|
readPendingDevice(pollId) {
|
|
620
|
-
const
|
|
621
|
-
|
|
685
|
+
const previous = readStoredAuth(stateFile);
|
|
686
|
+
const pending = [
|
|
687
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
688
|
+
...previous.pendingDevices ?? []
|
|
689
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
690
|
+
if (!pending)
|
|
622
691
|
return null;
|
|
623
692
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
624
693
|
return null;
|
|
625
694
|
return pending;
|
|
626
695
|
},
|
|
627
|
-
clearPendingDevice() {
|
|
696
|
+
clearPendingDevice(pollId) {
|
|
628
697
|
const previous = readStoredAuth(stateFile);
|
|
698
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
629
699
|
writeStoredAuth(stateFile, {
|
|
630
700
|
...previous,
|
|
631
701
|
pendingDevice: null,
|
|
702
|
+
pendingDevices: remaining,
|
|
632
703
|
updatedAt: new Date().toISOString()
|
|
633
704
|
});
|
|
634
705
|
}
|
|
635
706
|
};
|
|
636
707
|
}
|
|
708
|
+
function createGitHubAuthStore(projectRoot) {
|
|
709
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
710
|
+
}
|
|
637
711
|
|
|
638
712
|
// packages/server/src/server-helpers/github-projects.ts
|
|
639
713
|
function asRecord(value) {
|
|
@@ -752,6 +826,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
752
826
|
running: "In Progress",
|
|
753
827
|
prOpen: "In Review",
|
|
754
828
|
ciFixing: "In Review",
|
|
829
|
+
merging: "Merging",
|
|
755
830
|
done: "Done",
|
|
756
831
|
needsAttention: "Needs Attention"
|
|
757
832
|
};
|
|
@@ -765,6 +840,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
765
840
|
return "prOpen";
|
|
766
841
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
767
842
|
return "ciFixing";
|
|
843
|
+
if (normalized === "merging" || normalized === "merge")
|
|
844
|
+
return "merging";
|
|
768
845
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
769
846
|
return "needsAttention";
|
|
770
847
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -969,10 +1046,10 @@ var CLUSTERS = {
|
|
|
969
1046
|
};
|
|
970
1047
|
|
|
971
1048
|
// packages/server/src/server-helpers/task-config.ts
|
|
972
|
-
import { existsSync as
|
|
1049
|
+
import { existsSync as existsSync5 } from "fs";
|
|
973
1050
|
async function readTaskConfig(projectRoot) {
|
|
974
1051
|
const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
|
|
975
|
-
if (!
|
|
1052
|
+
if (!existsSync5(taskConfigPath)) {
|
|
976
1053
|
return {};
|
|
977
1054
|
}
|
|
978
1055
|
try {
|
|
@@ -989,8 +1066,8 @@ var serverPathEnvQueue = Promise.resolve();
|
|
|
989
1066
|
async function withServerPathEnv(projectRoot, fn) {
|
|
990
1067
|
const waitForTurn = serverPathEnvQueue;
|
|
991
1068
|
let releaseTurn;
|
|
992
|
-
serverPathEnvQueue = new Promise((
|
|
993
|
-
releaseTurn =
|
|
1069
|
+
serverPathEnvQueue = new Promise((resolve9) => {
|
|
1070
|
+
releaseTurn = resolve9;
|
|
994
1071
|
});
|
|
995
1072
|
await waitForTurn;
|
|
996
1073
|
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
@@ -1026,9 +1103,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
|
|
|
1026
1103
|
return withServerPathEnv(projectRoot, fn);
|
|
1027
1104
|
}
|
|
1028
1105
|
async function readWorkspaceTasks(projectRoot) {
|
|
1029
|
-
const issuesPath =
|
|
1106
|
+
const issuesPath = resolve8(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
|
|
1030
1107
|
const taskConfig = await readTaskConfig(projectRoot);
|
|
1031
|
-
if (!
|
|
1108
|
+
if (!existsSync6(issuesPath)) {
|
|
1032
1109
|
return [];
|
|
1033
1110
|
}
|
|
1034
1111
|
const latestById = new Map;
|
|
@@ -1074,7 +1151,7 @@ async function readWorkspaceTasks(projectRoot) {
|
|
|
1074
1151
|
if (false) {}
|
|
1075
1152
|
|
|
1076
1153
|
// packages/server/src/server-helpers/validation-failure.ts
|
|
1077
|
-
import { resolve as
|
|
1154
|
+
import { resolve as resolve9 } from "path";
|
|
1078
1155
|
import {
|
|
1079
1156
|
readJsonFile as readJsonFile4,
|
|
1080
1157
|
resolveTaskArtifactDirs
|
|
@@ -1088,7 +1165,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
1088
1165
|
continue;
|
|
1089
1166
|
}
|
|
1090
1167
|
seen.add(artifactRoot);
|
|
1091
|
-
const summary = readJsonFile4(
|
|
1168
|
+
const summary = readJsonFile4(resolve9(artifactRoot, "validation-summary.json"), null);
|
|
1092
1169
|
if (!summary || summary.status !== "fail") {
|
|
1093
1170
|
continue;
|
|
1094
1171
|
}
|
|
@@ -1168,9 +1245,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
1168
1245
|
return null;
|
|
1169
1246
|
return null;
|
|
1170
1247
|
}
|
|
1248
|
+
function githubProjectsEnabled(config) {
|
|
1249
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
1250
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
1251
|
+
return projects?.enabled === true;
|
|
1252
|
+
}
|
|
1171
1253
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
1172
1254
|
if (!run.taskId)
|
|
1173
|
-
return;
|
|
1255
|
+
return false;
|
|
1174
1256
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
1175
1257
|
try {
|
|
1176
1258
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -1181,28 +1263,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
1181
1263
|
config
|
|
1182
1264
|
});
|
|
1183
1265
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
1266
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
1184
1267
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
1185
1268
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
1186
1269
|
title: "GitHub Project sync skipped",
|
|
1187
|
-
detail
|
|
1270
|
+
detail,
|
|
1188
1271
|
tone: "warn",
|
|
1189
1272
|
status: "running",
|
|
1190
1273
|
createdAt: new Date().toISOString(),
|
|
1191
1274
|
payload: { reason: result.reason, issueNodeId }
|
|
1192
1275
|
});
|
|
1276
|
+
if (githubProjectsEnabled(config)) {
|
|
1277
|
+
throw new Error(detail);
|
|
1278
|
+
}
|
|
1279
|
+
return false;
|
|
1193
1280
|
}
|
|
1281
|
+
return result.synced === true;
|
|
1194
1282
|
} catch (error) {
|
|
1283
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1195
1284
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
1196
1285
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
1197
1286
|
title: "GitHub Project sync failed",
|
|
1198
|
-
detail
|
|
1287
|
+
detail,
|
|
1199
1288
|
tone: "error",
|
|
1200
1289
|
status: "running",
|
|
1201
1290
|
createdAt: new Date().toISOString(),
|
|
1202
1291
|
payload: { issueNodeId }
|
|
1203
1292
|
});
|
|
1293
|
+
if (githubProjectsEnabled(config)) {
|
|
1294
|
+
throw new Error(detail);
|
|
1295
|
+
}
|
|
1296
|
+
return false;
|
|
1204
1297
|
}
|
|
1205
1298
|
}
|
|
1299
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
1300
|
+
return async (args, options) => {
|
|
1301
|
+
const child = spawn(binary, [...args], {
|
|
1302
|
+
cwd: options?.cwd,
|
|
1303
|
+
env: { ...process.env, ...extraEnv },
|
|
1304
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1305
|
+
});
|
|
1306
|
+
const stdoutChunks = [];
|
|
1307
|
+
const stderrChunks = [];
|
|
1308
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
1309
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
1310
|
+
const exitCode = await new Promise((resolve11) => {
|
|
1311
|
+
child.once("error", () => resolve11(1));
|
|
1312
|
+
child.once("close", (code) => resolve11(code ?? 1));
|
|
1313
|
+
});
|
|
1314
|
+
return {
|
|
1315
|
+
exitCode,
|
|
1316
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
1317
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
1318
|
+
};
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function closeoutRecord(run) {
|
|
1322
|
+
const value = run.serverCloseout;
|
|
1323
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1324
|
+
}
|
|
1325
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
1326
|
+
const updatedAt = new Date().toISOString();
|
|
1327
|
+
return {
|
|
1328
|
+
serverCloseout: {
|
|
1329
|
+
phase,
|
|
1330
|
+
status,
|
|
1331
|
+
updatedAt,
|
|
1332
|
+
...extra
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
1337
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1338
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
1339
|
+
title: `Server closeout: ${phase}`,
|
|
1340
|
+
detail,
|
|
1341
|
+
tone,
|
|
1342
|
+
status,
|
|
1343
|
+
createdAt: new Date().toISOString()
|
|
1344
|
+
}, `server-closeout-${phase}`);
|
|
1345
|
+
}
|
|
1206
1346
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
1207
1347
|
if (!run.taskId)
|
|
1208
1348
|
return;
|
|
@@ -1232,7 +1372,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1232
1372
|
return;
|
|
1233
1373
|
}
|
|
1234
1374
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
1235
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
1375
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
1236
1376
|
if (status === "in_progress") {
|
|
1237
1377
|
await autoAssignRunIssue(projectRoot, run);
|
|
1238
1378
|
}
|
|
@@ -1248,24 +1388,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1248
1388
|
});
|
|
1249
1389
|
return;
|
|
1250
1390
|
}
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1391
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
1392
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
1393
|
+
const rollbackProjectSync = async () => {
|
|
1394
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
1395
|
+
return;
|
|
1396
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
1397
|
+
taskId: run.taskId,
|
|
1398
|
+
status: previousStatus,
|
|
1399
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
1400
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
1401
|
+
config
|
|
1402
|
+
}).catch((rollbackError) => {
|
|
1403
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
1404
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
1405
|
+
title: "GitHub Project sync rollback failed",
|
|
1406
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
1407
|
+
tone: "error",
|
|
1408
|
+
status: "running",
|
|
1409
|
+
createdAt: new Date().toISOString()
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
};
|
|
1413
|
+
let result;
|
|
1414
|
+
try {
|
|
1415
|
+
result = await updateConfiguredTaskSourceTask2(projectRoot, {
|
|
1416
|
+
taskId: run.taskId,
|
|
1417
|
+
sourceTask,
|
|
1418
|
+
update: {
|
|
1258
1419
|
status,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1420
|
+
comment: buildTaskRunLifecycleComment2({
|
|
1421
|
+
runId: run.runId,
|
|
1422
|
+
status,
|
|
1423
|
+
summary,
|
|
1424
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
1425
|
+
logsDir: normalizeString(run.logRoot),
|
|
1426
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
1427
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
1428
|
+
})
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
await rollbackProjectSync();
|
|
1433
|
+
throw error;
|
|
1434
|
+
}
|
|
1267
1435
|
if (!result.updated) {
|
|
1268
1436
|
if (result.source === "plugin" || result.sourceKind) {
|
|
1437
|
+
await rollbackProjectSync();
|
|
1269
1438
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
1270
1439
|
}
|
|
1271
1440
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -1278,6 +1447,219 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1278
1447
|
});
|
|
1279
1448
|
}
|
|
1280
1449
|
}
|
|
1450
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
1451
|
+
const run = readAuthorityRun8(state.projectRoot, runId);
|
|
1452
|
+
if (!run)
|
|
1453
|
+
throw new Error(`Run not found: ${runId}`);
|
|
1454
|
+
const closeout = closeoutRecord(run);
|
|
1455
|
+
if (!closeout)
|
|
1456
|
+
return;
|
|
1457
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
1458
|
+
if (!taskId)
|
|
1459
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
1460
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
1461
|
+
const branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
1462
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
1463
|
+
const runPrMode = normalizeString(run.prMode);
|
|
1464
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
1465
|
+
const effectiveConfig = {
|
|
1466
|
+
...config ?? {},
|
|
1467
|
+
pr: {
|
|
1468
|
+
...config?.pr ?? {},
|
|
1469
|
+
mode: prMode,
|
|
1470
|
+
autoFixChecks: false,
|
|
1471
|
+
autoFixReview: false
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const readCurrentRun = () => readAuthorityRun8(state.projectRoot, runId) ?? run;
|
|
1475
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
1476
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
1477
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
1478
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
1479
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
1483
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1484
|
+
status: "reviewing",
|
|
1485
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
1486
|
+
});
|
|
1487
|
+
await closeIssueAfterMergedPr({
|
|
1488
|
+
projectRoot: state.projectRoot,
|
|
1489
|
+
taskId,
|
|
1490
|
+
runId,
|
|
1491
|
+
prUrl: closeoutPrUrl,
|
|
1492
|
+
sourceTask,
|
|
1493
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
1494
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
1495
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
const completedAt = new Date().toISOString();
|
|
1499
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1500
|
+
status: "completed",
|
|
1501
|
+
completedAt,
|
|
1502
|
+
errorText: null,
|
|
1503
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
1504
|
+
});
|
|
1505
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
1506
|
+
emitRigEvent(state, {
|
|
1507
|
+
type: "rig.run.completed",
|
|
1508
|
+
aggregateId: runId,
|
|
1509
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
1510
|
+
createdAt: completedAt
|
|
1511
|
+
});
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (prMode === "off" || prMode === "ask") {
|
|
1515
|
+
const completedAt = new Date().toISOString();
|
|
1516
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1517
|
+
status: "completed",
|
|
1518
|
+
completedAt,
|
|
1519
|
+
errorText: null,
|
|
1520
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
1521
|
+
});
|
|
1522
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
1523
|
+
emitRigEvent(state, {
|
|
1524
|
+
type: "rig.run.completed",
|
|
1525
|
+
aggregateId: runId,
|
|
1526
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
1527
|
+
createdAt: completedAt
|
|
1528
|
+
});
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
1532
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
1533
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
1534
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
1535
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
1536
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
1537
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1538
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
1539
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
1540
|
+
});
|
|
1541
|
+
};
|
|
1542
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
1543
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
1544
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
1545
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
1546
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
1547
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
1548
|
+
if (push.exitCode !== 0) {
|
|
1549
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
1550
|
+
}
|
|
1551
|
+
const sourceTaskForPr = {
|
|
1552
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
1553
|
+
};
|
|
1554
|
+
const artifactRoot = resolve10(state.projectRoot, "artifacts", taskId);
|
|
1555
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1556
|
+
const pr = await runPrAutomation({
|
|
1557
|
+
projectRoot: workspace,
|
|
1558
|
+
taskId,
|
|
1559
|
+
runId,
|
|
1560
|
+
branch,
|
|
1561
|
+
config: effectiveConfig,
|
|
1562
|
+
sourceTask: sourceTaskForPr,
|
|
1563
|
+
artifactRoot,
|
|
1564
|
+
command: ghCommand,
|
|
1565
|
+
gitCommand,
|
|
1566
|
+
steerPi: async (message) => {
|
|
1567
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
1568
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
1569
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
1570
|
+
type: "user_message",
|
|
1571
|
+
text: message,
|
|
1572
|
+
createdAt: new Date().toISOString(),
|
|
1573
|
+
state: "completed"
|
|
1574
|
+
});
|
|
1575
|
+
},
|
|
1576
|
+
lifecycle: {
|
|
1577
|
+
onPrOpened: async ({ prUrl }) => {
|
|
1578
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1579
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
1580
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
1581
|
+
},
|
|
1582
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
1583
|
+
onFeedback: async ({ feedback }) => {
|
|
1584
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
1585
|
+
`), "reviewing", "error");
|
|
1586
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
1587
|
+
},
|
|
1588
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
1589
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1590
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
1591
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
1592
|
+
},
|
|
1593
|
+
onMerged: ({ prUrl }) => {
|
|
1594
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
1595
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
1600
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
1601
|
+
await closeIssueAfterMergedPr({
|
|
1602
|
+
projectRoot: state.projectRoot,
|
|
1603
|
+
taskId,
|
|
1604
|
+
runId,
|
|
1605
|
+
prUrl: pr.prUrl,
|
|
1606
|
+
sourceTask,
|
|
1607
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
1608
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
1609
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
const completedAt = new Date().toISOString();
|
|
1613
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1614
|
+
status: "completed",
|
|
1615
|
+
completedAt,
|
|
1616
|
+
errorText: null,
|
|
1617
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
1618
|
+
});
|
|
1619
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
1620
|
+
emitRigEvent(state, {
|
|
1621
|
+
type: "rig.run.completed",
|
|
1622
|
+
aggregateId: runId,
|
|
1623
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
1624
|
+
createdAt: completedAt
|
|
1625
|
+
});
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
1629
|
+
const completedAt = new Date().toISOString();
|
|
1630
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1631
|
+
status: "completed",
|
|
1632
|
+
completedAt,
|
|
1633
|
+
errorText: null,
|
|
1634
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
1635
|
+
});
|
|
1636
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
1637
|
+
emitRigEvent(state, {
|
|
1638
|
+
type: "rig.run.completed",
|
|
1639
|
+
aggregateId: runId,
|
|
1640
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
1641
|
+
createdAt: completedAt
|
|
1642
|
+
});
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const detail = pr.actionableFeedback.join(`
|
|
1646
|
+
`) || "PR automation did not merge the PR.";
|
|
1647
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
1648
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
1649
|
+
});
|
|
1650
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1651
|
+
status: "needs_attention",
|
|
1652
|
+
completedAt: new Date().toISOString(),
|
|
1653
|
+
errorText: detail,
|
|
1654
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
1655
|
+
});
|
|
1656
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
1657
|
+
emitRigEvent(state, {
|
|
1658
|
+
type: "rig.run.needs-attention",
|
|
1659
|
+
aggregateId: runId,
|
|
1660
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1281
1663
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
1282
1664
|
"completed",
|
|
1283
1665
|
"complete",
|
|
@@ -1303,11 +1685,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
1303
1685
|
return;
|
|
1304
1686
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
1305
1687
|
}
|
|
1688
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
1689
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
1690
|
+
if (fromReader)
|
|
1691
|
+
return fromReader;
|
|
1692
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
1693
|
+
if (projected)
|
|
1694
|
+
return projected;
|
|
1695
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
1696
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
1697
|
+
}
|
|
1698
|
+
return null;
|
|
1699
|
+
}
|
|
1306
1700
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
1307
1701
|
if ("taskId" in input && input.taskId) {
|
|
1308
1702
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
1309
1703
|
}
|
|
1310
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
1704
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
1311
1705
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
1312
1706
|
const runDir = resolveAuthorityRunDir4(projectRoot, input.runId);
|
|
1313
1707
|
const runRecord = {
|
|
@@ -1341,11 +1735,11 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
|
|
|
1341
1735
|
initiatedBy: input.initiatedBy ?? null,
|
|
1342
1736
|
...sourceTask ? { sourceTask: sourceTaskContract(sourceTask) } : {}
|
|
1343
1737
|
};
|
|
1344
|
-
|
|
1345
|
-
|
|
1738
|
+
mkdirSync6(runDir, { recursive: true });
|
|
1739
|
+
writeFileSync6(resolve10(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
|
|
1346
1740
|
`, "utf8");
|
|
1347
1741
|
if ("initialPrompt" in input && input.initialPrompt && input.initialPrompt.trim().length > 0) {
|
|
1348
|
-
|
|
1742
|
+
writeFileSync6(resolve10(runDir, "timeline.jsonl"), `${JSON.stringify({
|
|
1349
1743
|
id: `message-${Date.now()}`,
|
|
1350
1744
|
type: "user_message",
|
|
1351
1745
|
text: input.initialPrompt,
|
|
@@ -1379,6 +1773,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
1379
1773
|
throw new Error(`Run not found: ${runId}`);
|
|
1380
1774
|
}
|
|
1381
1775
|
const startedAt = new Date().toISOString();
|
|
1776
|
+
const resumeMode = options?.resume === true;
|
|
1382
1777
|
state.runProcesses.set(runId, {
|
|
1383
1778
|
runId,
|
|
1384
1779
|
child: null,
|
|
@@ -1395,9 +1790,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
1395
1790
|
summary: run.title
|
|
1396
1791
|
});
|
|
1397
1792
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
1398
|
-
id: `log:${runId}:prepare`,
|
|
1399
|
-
title: "Rig task run starting",
|
|
1400
|
-
detail: run.taskId ?? run.title,
|
|
1793
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
1794
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
1795
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
1401
1796
|
tone: "info",
|
|
1402
1797
|
status: "preparing",
|
|
1403
1798
|
createdAt: startedAt
|
|
@@ -1405,8 +1800,8 @@ async function startLocalRun(state, runId, options) {
|
|
|
1405
1800
|
broadcastRunLogAppended(state, runId, readLatestRawRunLog(state.projectRoot, runId));
|
|
1406
1801
|
broadcastSnapshotInvalidation(state);
|
|
1407
1802
|
const cliProjectRoot = resolveLocalRunCliProjectRoot(state.projectRoot);
|
|
1408
|
-
const cliEntryPoint =
|
|
1409
|
-
if (!
|
|
1803
|
+
const cliEntryPoint = resolve10(cliProjectRoot, "packages/cli/bin/rig.ts");
|
|
1804
|
+
if (!existsSync7(cliEntryPoint)) {
|
|
1410
1805
|
const completedAt = new Date().toISOString();
|
|
1411
1806
|
const failureSummary = `Rig task-run entrypoint missing at ${relative2(state.projectRoot, cliEntryPoint)}`;
|
|
1412
1807
|
patchRunRecord(state.projectRoot, runId, {
|
|
@@ -1473,9 +1868,18 @@ async function startLocalRun(state, runId, options) {
|
|
|
1473
1868
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
1474
1869
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
1475
1870
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
1871
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
1476
1872
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
1477
1873
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
1478
|
-
...bridgeGitHubToken ? {
|
|
1874
|
+
...bridgeGitHubToken ? {
|
|
1875
|
+
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
1876
|
+
GITHUB_TOKEN: bridgeGitHubToken,
|
|
1877
|
+
GH_TOKEN: bridgeGitHubToken
|
|
1878
|
+
} : {},
|
|
1879
|
+
...resumeMode ? {
|
|
1880
|
+
RIG_RUN_RESUME: "1",
|
|
1881
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
1882
|
+
} : {}
|
|
1479
1883
|
},
|
|
1480
1884
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1481
1885
|
});
|
|
@@ -1515,9 +1919,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
1515
1919
|
handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
|
|
1516
1920
|
});
|
|
1517
1921
|
try {
|
|
1518
|
-
const exit = await new Promise((
|
|
1519
|
-
child.once("error", (error) =>
|
|
1520
|
-
child.once("close", (code, signal) =>
|
|
1922
|
+
const exit = await new Promise((resolve11) => {
|
|
1923
|
+
child.once("error", (error) => resolve11({ code: 1, signal: null, error }));
|
|
1924
|
+
child.once("close", (code, signal) => resolve11({ code, signal }));
|
|
1521
1925
|
});
|
|
1522
1926
|
if (exit.error) {
|
|
1523
1927
|
throw new Error(`Failed to start task run: ${exit.error.message}`);
|
|
@@ -1561,6 +1965,38 @@ ${sourceFailure}` });
|
|
|
1561
1965
|
agent: current.runtimeAdapter,
|
|
1562
1966
|
summary: failureSummary
|
|
1563
1967
|
});
|
|
1968
|
+
} else if (closeoutRecord(current)?.status === "pending") {
|
|
1969
|
+
try {
|
|
1970
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
1971
|
+
} catch (closeoutError) {
|
|
1972
|
+
const closeoutFailure = closeoutError instanceof Error ? closeoutError.message : String(closeoutError);
|
|
1973
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1974
|
+
status: "failed",
|
|
1975
|
+
completedAt: new Date().toISOString(),
|
|
1976
|
+
errorText: closeoutFailure,
|
|
1977
|
+
...closeoutPhasePatch("failed", "failed", { error: closeoutFailure })
|
|
1978
|
+
});
|
|
1979
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1980
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
1981
|
+
title: "Server-owned closeout failed",
|
|
1982
|
+
detail: closeoutFailure,
|
|
1983
|
+
tone: "error",
|
|
1984
|
+
status: "failed",
|
|
1985
|
+
createdAt: new Date().toISOString()
|
|
1986
|
+
}, "server-closeout-failed");
|
|
1987
|
+
if (current.taskId) {
|
|
1988
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: closeoutFailure }, "failed", "Rig server-owned closeout failed.", { errorText: closeoutFailure }).catch((error) => {
|
|
1989
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
1990
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
1991
|
+
title: "Task source closeout failure update failed",
|
|
1992
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
1993
|
+
tone: "error",
|
|
1994
|
+
status: "failed",
|
|
1995
|
+
createdAt: new Date().toISOString()
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1564
2000
|
}
|
|
1565
2001
|
broadcastSnapshotInvalidation(state);
|
|
1566
2002
|
} catch (error) {
|
|
@@ -1617,17 +2053,17 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
|
|
|
1617
2053
|
process.env.PROJECT_RIG_ROOT?.trim()
|
|
1618
2054
|
].filter((value) => !!value);
|
|
1619
2055
|
for (const candidate of envCandidates) {
|
|
1620
|
-
if (
|
|
1621
|
-
return
|
|
2056
|
+
if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
|
|
2057
|
+
return resolve10(candidate);
|
|
1622
2058
|
}
|
|
1623
2059
|
}
|
|
1624
|
-
if (
|
|
2060
|
+
if (existsSync7(resolve10(projectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1625
2061
|
return projectRoot;
|
|
1626
2062
|
}
|
|
1627
2063
|
try {
|
|
1628
2064
|
const monorepoRoot = resolveMonorepoRoot6(projectRoot);
|
|
1629
|
-
const outerProjectRoot =
|
|
1630
|
-
if (
|
|
2065
|
+
const outerProjectRoot = dirname6(dirname6(monorepoRoot));
|
|
2066
|
+
if (existsSync7(resolve10(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1631
2067
|
return outerProjectRoot;
|
|
1632
2068
|
}
|
|
1633
2069
|
} catch {}
|
|
@@ -1647,15 +2083,21 @@ async function resumeRunRecord(state, input) {
|
|
|
1647
2083
|
if (run.status === "completed") {
|
|
1648
2084
|
throw new Error("Completed runs cannot be resumed.");
|
|
1649
2085
|
}
|
|
1650
|
-
|
|
2086
|
+
const closeout = closeoutRecord(run);
|
|
2087
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
2088
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
2089
|
+
await runServerOwnedPrCloseout(state, input.runId);
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
1651
2093
|
}
|
|
1652
2094
|
function appendRunMessage(projectRoot, input) {
|
|
1653
2095
|
const run = readAuthorityRun8(projectRoot, input.runId);
|
|
1654
2096
|
if (!run) {
|
|
1655
2097
|
throw new Error(`Run not found: ${input.runId}`);
|
|
1656
2098
|
}
|
|
1657
|
-
const timelinePath =
|
|
1658
|
-
const existingLines = fileExists(timelinePath) ?
|
|
2099
|
+
const timelinePath = resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
|
|
2100
|
+
const existingLines = fileExists(timelinePath) ? readFileSync4(timelinePath, "utf8").trim() : "";
|
|
1659
2101
|
const nextLine = JSON.stringify({
|
|
1660
2102
|
id: input.messageId,
|
|
1661
2103
|
type: "user_message",
|
|
@@ -1663,11 +2105,11 @@ function appendRunMessage(projectRoot, input) {
|
|
|
1663
2105
|
attachments: input.attachments ?? [],
|
|
1664
2106
|
createdAt: input.createdAt
|
|
1665
2107
|
});
|
|
1666
|
-
|
|
2108
|
+
writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
|
|
1667
2109
|
${nextLine}
|
|
1668
2110
|
` : `${nextLine}
|
|
1669
2111
|
`, "utf8");
|
|
1670
|
-
writeJsonFile4(
|
|
2112
|
+
writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
|
|
1671
2113
|
...run,
|
|
1672
2114
|
updatedAt: input.createdAt
|
|
1673
2115
|
});
|
|
@@ -1693,7 +2135,7 @@ async function stopRunRecord(stateOrProjectRoot, input) {
|
|
|
1693
2135
|
completedAt: run.completedAt ?? input.createdAt,
|
|
1694
2136
|
updatedAt: input.createdAt
|
|
1695
2137
|
};
|
|
1696
|
-
writeJsonFile4(
|
|
2138
|
+
writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
|
|
1697
2139
|
if (run.status !== "completed" && run.taskId) {
|
|
1698
2140
|
const taskId = run.taskId;
|
|
1699
2141
|
(async () => {
|
|
@@ -1731,34 +2173,46 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
|
|
|
1731
2173
|
writeQueueState(projectRoot, next);
|
|
1732
2174
|
return next;
|
|
1733
2175
|
}
|
|
1734
|
-
var
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
1745
|
-
patchRunRecord(state.projectRoot, run.runId, {
|
|
1746
|
-
status: "failed",
|
|
1747
|
-
completedAt: run.completedAt ?? nowIso,
|
|
1748
|
-
updatedAt: nowIso,
|
|
1749
|
-
errorText: detail
|
|
1750
|
-
});
|
|
1751
|
-
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
1752
|
-
id: `log:${run.runId}:stale-local-run`,
|
|
1753
|
-
title: "Run marked stale after server restart",
|
|
1754
|
-
detail,
|
|
1755
|
-
tone: "error",
|
|
1756
|
-
status: "failed",
|
|
1757
|
-
createdAt: nowIso
|
|
1758
|
-
});
|
|
1759
|
-
changed = true;
|
|
2176
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
2177
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
2178
|
+
function processExists(pid) {
|
|
2179
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
2180
|
+
return false;
|
|
2181
|
+
try {
|
|
2182
|
+
process.kill(pid, 0);
|
|
2183
|
+
return true;
|
|
2184
|
+
} catch {
|
|
2185
|
+
return false;
|
|
1760
2186
|
}
|
|
1761
|
-
|
|
2187
|
+
}
|
|
2188
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
2189
|
+
const record = run;
|
|
2190
|
+
if (run.mode !== "local")
|
|
2191
|
+
return false;
|
|
2192
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
2193
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
2194
|
+
return false;
|
|
2195
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
2196
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
2197
|
+
if (serverPid === null && childPid === null)
|
|
2198
|
+
return false;
|
|
2199
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
2200
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
2201
|
+
return false;
|
|
2202
|
+
const completedAt = new Date().toISOString();
|
|
2203
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
2204
|
+
status: "failed",
|
|
2205
|
+
completedAt,
|
|
2206
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
2207
|
+
});
|
|
2208
|
+
return true;
|
|
2209
|
+
}
|
|
2210
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
2211
|
+
return runs.filter((run) => {
|
|
2212
|
+
const closeout = closeoutRecord(run);
|
|
2213
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
2214
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
2215
|
+
});
|
|
1762
2216
|
}
|
|
1763
2217
|
async function reconcileScheduler(state, reason) {
|
|
1764
2218
|
if (state.scheduler.reconciling) {
|
|
@@ -1773,7 +2227,36 @@ async function reconcileScheduler(state, reason) {
|
|
|
1773
2227
|
const queue = readQueueState(state.projectRoot);
|
|
1774
2228
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
1775
2229
|
let runs = listAuthorityRuns7(state.projectRoot);
|
|
1776
|
-
let changed =
|
|
2230
|
+
let changed = false;
|
|
2231
|
+
for (const run of runs) {
|
|
2232
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
2233
|
+
changed = true;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (changed) {
|
|
2237
|
+
runs = listAuthorityRuns7(state.projectRoot);
|
|
2238
|
+
}
|
|
2239
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
2240
|
+
for (const run of resumableCloseouts) {
|
|
2241
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
2242
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
2243
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
2244
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
2245
|
+
tone: "info",
|
|
2246
|
+
status: "reviewing",
|
|
2247
|
+
createdAt: new Date().toISOString()
|
|
2248
|
+
});
|
|
2249
|
+
await runServerOwnedPrCloseout(state, run.runId).catch((error) => {
|
|
2250
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2251
|
+
patchRunRecord(state.projectRoot, run.runId, {
|
|
2252
|
+
status: "failed",
|
|
2253
|
+
completedAt: new Date().toISOString(),
|
|
2254
|
+
errorText: detail,
|
|
2255
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
changed = true;
|
|
2259
|
+
}
|
|
1777
2260
|
if (changed) {
|
|
1778
2261
|
runs = listAuthorityRuns7(state.projectRoot);
|
|
1779
2262
|
}
|
|
@@ -1846,8 +2329,10 @@ async function reconcileScheduler(state, reason) {
|
|
|
1846
2329
|
}
|
|
1847
2330
|
}
|
|
1848
2331
|
export {
|
|
2332
|
+
updateRunTaskSourceLifecycle,
|
|
1849
2333
|
stopRunRecord,
|
|
1850
2334
|
startLocalRun,
|
|
2335
|
+
runServerOwnedPrCloseout,
|
|
1851
2336
|
resumeRunRecord,
|
|
1852
2337
|
resolveLocalRunCliProjectRoot,
|
|
1853
2338
|
removeTaskIdsFromQueueState2 as removeTaskIdsFromQueueState,
|