@h-rig/server 0.0.6-alpha.3 → 0.0.6-alpha.30
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 +1688 -351
- 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 +1040 -200
- package/dist/src/server-helpers/inspector-jobs.js +1 -12
- package/dist/src/server-helpers/issue-analysis.js +26 -10
- package/dist/src/server-helpers/pi-session-proxy.js +84 -0
- package/dist/src/server-helpers/project-registry.js +5 -0
- package/dist/src/server-helpers/run-io.js +30 -1
- package/dist/src/server-helpers/run-mutations.js +679 -123
- package/dist/src/server-helpers/run-writers.js +8 -2
- package/dist/src/server-helpers/ws-router.js +16 -6
- package/dist/src/server.js +1687 -351
- package/package.json +4 -4
|
@@ -2,11 +2,11 @@
|
|
|
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
|
-
readAuthorityRun as
|
|
9
|
+
readAuthorityRun as readAuthorityRun9,
|
|
10
10
|
resolveAuthorityRunDir as resolveAuthorityRunDir4,
|
|
11
11
|
writeJsonFile as writeJsonFile4
|
|
12
12
|
} from "@rig/runtime/control-plane/authority-files";
|
|
@@ -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,16 +360,21 @@ 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
|
}
|
|
366
|
+
function patchRunPiSessionMetadata(projectRoot, runId, metadata) {
|
|
367
|
+
return patchRunRecord(projectRoot, runId, {
|
|
368
|
+
piSession: metadata?.public ?? null,
|
|
369
|
+
piSessionPrivate: metadata
|
|
370
|
+
});
|
|
371
|
+
}
|
|
333
372
|
function buildRunStartPatch(startedAt) {
|
|
334
373
|
return {
|
|
335
374
|
status: "preparing",
|
|
336
375
|
startedAt,
|
|
337
376
|
completedAt: null,
|
|
338
|
-
errorText: null
|
|
339
|
-
serverPid: process.pid
|
|
377
|
+
errorText: null
|
|
340
378
|
};
|
|
341
379
|
}
|
|
342
380
|
|
|
@@ -428,7 +466,7 @@ var DEFAULT_TERMINAL_SHELL = process.env.SHELL || "/bin/zsh";
|
|
|
428
466
|
// packages/server/src/server-helpers/http-router.ts
|
|
429
467
|
import {
|
|
430
468
|
listAuthorityRuns as listAuthorityRuns4,
|
|
431
|
-
readAuthorityRun as
|
|
469
|
+
readAuthorityRun as readAuthorityRun6,
|
|
432
470
|
resolveAuthorityPaths,
|
|
433
471
|
writeJsonFile as writeJsonFile3
|
|
434
472
|
} from "@rig/runtime/control-plane/authority-files";
|
|
@@ -448,8 +486,11 @@ import {
|
|
|
448
486
|
RemoteWsClient
|
|
449
487
|
} from "@rig/runtime/control-plane/remote";
|
|
450
488
|
|
|
489
|
+
// packages/server/src/server-helpers/pi-session-proxy.ts
|
|
490
|
+
import { readAuthorityRun as readAuthorityRun4 } from "@rig/runtime/control-plane/authority-files";
|
|
491
|
+
|
|
451
492
|
// packages/server/src/server-helpers/run-steering.ts
|
|
452
|
-
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as
|
|
493
|
+
import { appendJsonlRecord as appendJsonlRecord2, readAuthorityRun as readAuthorityRun5, resolveAuthorityRunDir as resolveAuthorityRunDir3 } from "@rig/runtime/control-plane/authority-files";
|
|
453
494
|
|
|
454
495
|
// packages/server/src/server-helpers/http-router.ts
|
|
455
496
|
import { buildRigInitConfigSource } from "@rig/core";
|
|
@@ -460,8 +501,8 @@ import {
|
|
|
460
501
|
|
|
461
502
|
// packages/server/src/server-helpers/github-auth-store.ts
|
|
462
503
|
import { randomBytes } from "crypto";
|
|
463
|
-
import { chmodSync, existsSync as
|
|
464
|
-
import { resolve as
|
|
504
|
+
import { chmodSync, copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
505
|
+
import { dirname as dirname4, resolve as resolve7 } from "path";
|
|
465
506
|
function cleanString(value) {
|
|
466
507
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
467
508
|
}
|
|
@@ -491,11 +532,31 @@ function parseApiSessions(value) {
|
|
|
491
532
|
}];
|
|
492
533
|
});
|
|
493
534
|
}
|
|
535
|
+
function parsePendingDevice(value) {
|
|
536
|
+
if (!value || typeof value !== "object")
|
|
537
|
+
return null;
|
|
538
|
+
const record = value;
|
|
539
|
+
const pollId = cleanString(record.pollId);
|
|
540
|
+
const deviceCode = cleanString(record.deviceCode);
|
|
541
|
+
const expiresAt = cleanString(record.expiresAt);
|
|
542
|
+
const intervalSeconds = typeof record.intervalSeconds === "number" && Number.isFinite(record.intervalSeconds) ? Math.max(1, Math.floor(record.intervalSeconds)) : null;
|
|
543
|
+
if (!pollId || !deviceCode || !expiresAt || !intervalSeconds)
|
|
544
|
+
return null;
|
|
545
|
+
return { pollId, deviceCode, expiresAt, intervalSeconds };
|
|
546
|
+
}
|
|
547
|
+
function parsePendingDevices(value) {
|
|
548
|
+
if (!Array.isArray(value))
|
|
549
|
+
return [];
|
|
550
|
+
return value.flatMap((entry) => {
|
|
551
|
+
const pending = parsePendingDevice(entry);
|
|
552
|
+
return pending ? [pending] : [];
|
|
553
|
+
});
|
|
554
|
+
}
|
|
494
555
|
function readStoredAuth(stateFile) {
|
|
495
|
-
if (!
|
|
556
|
+
if (!existsSync4(stateFile))
|
|
496
557
|
return {};
|
|
497
558
|
try {
|
|
498
|
-
const parsed = JSON.parse(
|
|
559
|
+
const parsed = JSON.parse(readFileSync2(stateFile, "utf8"));
|
|
499
560
|
return {
|
|
500
561
|
...cleanString(parsed.token) ? { token: cleanString(parsed.token) } : {},
|
|
501
562
|
login: cleanString(parsed.login),
|
|
@@ -504,6 +565,7 @@ function readStoredAuth(stateFile) {
|
|
|
504
565
|
selectedRepo: cleanString(parsed.selectedRepo),
|
|
505
566
|
tokenSource: parsed.tokenSource === "oauth-device" || parsed.tokenSource === "manual-token" || parsed.tokenSource === "env" ? parsed.tokenSource : undefined,
|
|
506
567
|
pendingDevice: parsePendingDevice(parsed.pendingDevice),
|
|
568
|
+
pendingDevices: parsePendingDevices(parsed.pendingDevices),
|
|
507
569
|
apiSessions: parseApiSessions(parsed.apiSessions),
|
|
508
570
|
updatedAt: cleanString(parsed.updatedAt) ?? undefined
|
|
509
571
|
};
|
|
@@ -511,34 +573,36 @@ function readStoredAuth(stateFile) {
|
|
|
511
573
|
return {};
|
|
512
574
|
}
|
|
513
575
|
}
|
|
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
576
|
function newApiSessionToken() {
|
|
527
577
|
return `rig_${randomBytes(32).toString("base64url")}`;
|
|
528
578
|
}
|
|
529
579
|
function writeStoredAuth(stateFile, payload) {
|
|
530
|
-
|
|
531
|
-
|
|
580
|
+
mkdirSync4(dirname4(stateFile), { recursive: true });
|
|
581
|
+
writeFileSync4(stateFile, `${JSON.stringify(payload, null, 2)}
|
|
532
582
|
`, { encoding: "utf8", mode: 384 });
|
|
533
583
|
try {
|
|
534
584
|
chmodSync(stateFile, 384);
|
|
535
585
|
} catch {}
|
|
536
586
|
}
|
|
587
|
+
function localProjectAuthStateFile(projectRoot) {
|
|
588
|
+
return resolve7(projectRoot, ".rig", "state", "github-auth.json");
|
|
589
|
+
}
|
|
537
590
|
function resolveGitHubAuthStateFile(projectRoot) {
|
|
538
|
-
return
|
|
591
|
+
return resolve7(resolveServerAuthorityPaths(projectRoot).stateDir, "github-auth.json");
|
|
539
592
|
}
|
|
540
|
-
function
|
|
541
|
-
const
|
|
593
|
+
function copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot) {
|
|
594
|
+
const targetFile = localProjectAuthStateFile(projectRoot);
|
|
595
|
+
mkdirSync4(dirname4(targetFile), { recursive: true });
|
|
596
|
+
if (existsSync4(stateFile)) {
|
|
597
|
+
copyFileSync(stateFile, targetFile);
|
|
598
|
+
try {
|
|
599
|
+
chmodSync(targetFile, 384);
|
|
600
|
+
} catch {}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
writeStoredAuth(targetFile, {});
|
|
604
|
+
}
|
|
605
|
+
function createGitHubAuthStoreFromStateFile(stateFile) {
|
|
542
606
|
return {
|
|
543
607
|
stateFile,
|
|
544
608
|
status(options) {
|
|
@@ -568,6 +632,7 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
568
632
|
scopes: input.scopes ?? [],
|
|
569
633
|
selectedRepo: input.selectedRepo ?? previous.selectedRepo ?? null,
|
|
570
634
|
pendingDevice: null,
|
|
635
|
+
pendingDevices: [],
|
|
571
636
|
apiSessions: previous.apiSessions ?? [],
|
|
572
637
|
updatedAt: new Date().toISOString()
|
|
573
638
|
});
|
|
@@ -596,15 +661,24 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
596
661
|
const session = (previous.apiSessions ?? []).find((candidate) => candidate.token === clean);
|
|
597
662
|
return session ? { login: cleanString(session.login), userId: cleanString(session.userId) } : null;
|
|
598
663
|
},
|
|
599
|
-
copyToProjectRoot(
|
|
600
|
-
const targetFile = resolveGitHubAuthStateFile(
|
|
664
|
+
copyToProjectRoot(projectRoot) {
|
|
665
|
+
const targetFile = resolveGitHubAuthStateFile(projectRoot);
|
|
601
666
|
writeStoredAuth(targetFile, readStoredAuth(stateFile));
|
|
602
667
|
},
|
|
668
|
+
copyToLocalProjectRoot(projectRoot) {
|
|
669
|
+
copyGitHubAuthStateToLocalProjectRoot(stateFile, projectRoot);
|
|
670
|
+
},
|
|
603
671
|
savePendingDevice(input) {
|
|
604
672
|
const previous = readStoredAuth(stateFile);
|
|
673
|
+
const pendingDevices = [
|
|
674
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
675
|
+
...previous.pendingDevices ?? [],
|
|
676
|
+
input
|
|
677
|
+
].filter((entry, index, entries) => entries.findIndex((candidate) => candidate.pollId === entry.pollId) === index);
|
|
605
678
|
writeStoredAuth(stateFile, {
|
|
606
679
|
...previous,
|
|
607
|
-
pendingDevice:
|
|
680
|
+
pendingDevice: null,
|
|
681
|
+
pendingDevices,
|
|
608
682
|
updatedAt: new Date().toISOString()
|
|
609
683
|
});
|
|
610
684
|
},
|
|
@@ -617,23 +691,32 @@ function createGitHubAuthStore(projectRoot) {
|
|
|
617
691
|
});
|
|
618
692
|
},
|
|
619
693
|
readPendingDevice(pollId) {
|
|
620
|
-
const
|
|
621
|
-
|
|
694
|
+
const previous = readStoredAuth(stateFile);
|
|
695
|
+
const pending = [
|
|
696
|
+
...previous.pendingDevice ? [previous.pendingDevice] : [],
|
|
697
|
+
...previous.pendingDevices ?? []
|
|
698
|
+
].find((entry) => entry.pollId === pollId) ?? null;
|
|
699
|
+
if (!pending)
|
|
622
700
|
return null;
|
|
623
701
|
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
624
702
|
return null;
|
|
625
703
|
return pending;
|
|
626
704
|
},
|
|
627
|
-
clearPendingDevice() {
|
|
705
|
+
clearPendingDevice(pollId) {
|
|
628
706
|
const previous = readStoredAuth(stateFile);
|
|
707
|
+
const remaining = pollId ? (previous.pendingDevices ?? []).filter((entry) => entry.pollId !== pollId) : [];
|
|
629
708
|
writeStoredAuth(stateFile, {
|
|
630
709
|
...previous,
|
|
631
710
|
pendingDevice: null,
|
|
711
|
+
pendingDevices: remaining,
|
|
632
712
|
updatedAt: new Date().toISOString()
|
|
633
713
|
});
|
|
634
714
|
}
|
|
635
715
|
};
|
|
636
716
|
}
|
|
717
|
+
function createGitHubAuthStore(projectRoot) {
|
|
718
|
+
return createGitHubAuthStoreFromStateFile(resolveGitHubAuthStateFile(projectRoot));
|
|
719
|
+
}
|
|
637
720
|
|
|
638
721
|
// packages/server/src/server-helpers/github-projects.ts
|
|
639
722
|
function asRecord(value) {
|
|
@@ -752,6 +835,7 @@ var DEFAULT_PROJECT_STATUSES = {
|
|
|
752
835
|
running: "In Progress",
|
|
753
836
|
prOpen: "In Review",
|
|
754
837
|
ciFixing: "In Review",
|
|
838
|
+
merging: "Merging",
|
|
755
839
|
done: "Done",
|
|
756
840
|
needsAttention: "Needs Attention"
|
|
757
841
|
};
|
|
@@ -765,6 +849,8 @@ function lifecycleStatusForTaskStatus(status) {
|
|
|
765
849
|
return "prOpen";
|
|
766
850
|
if (normalized === "ci_fixing" || normalized === "fixing")
|
|
767
851
|
return "ciFixing";
|
|
852
|
+
if (normalized === "merging" || normalized === "merge")
|
|
853
|
+
return "merging";
|
|
768
854
|
if (normalized === "failed" || normalized === "needs_attention" || normalized === "blocked")
|
|
769
855
|
return "needsAttention";
|
|
770
856
|
if (normalized === "in_progress" || normalized === "running" || normalized === "ready" || normalized === "open")
|
|
@@ -848,7 +934,7 @@ import {
|
|
|
848
934
|
RemoteWsClient as RemoteWsClient2
|
|
849
935
|
} from "@rig/runtime/control-plane/remote";
|
|
850
936
|
import { deleteRunState } from "@rig/runtime/control-plane/native/run-ops";
|
|
851
|
-
import { readAuthorityRun as
|
|
937
|
+
import { readAuthorityRun as readAuthorityRun7 } from "@rig/runtime/control-plane/authority-files";
|
|
852
938
|
|
|
853
939
|
// packages/server/src/server-helpers/inspector-jobs.ts
|
|
854
940
|
import { readJsonFile as readJsonFile2 } from "@rig/runtime/control-plane/authority-files";
|
|
@@ -861,7 +947,7 @@ import {
|
|
|
861
947
|
} from "@rig/runtime/control-plane/native/run-ops";
|
|
862
948
|
import {
|
|
863
949
|
listAuthorityRuns as listAuthorityRuns5,
|
|
864
|
-
readAuthorityRun as
|
|
950
|
+
readAuthorityRun as readAuthorityRun8
|
|
865
951
|
} from "@rig/runtime/control-plane/authority-files";
|
|
866
952
|
|
|
867
953
|
// packages/server/src/inspector/service.ts
|
|
@@ -969,10 +1055,10 @@ var CLUSTERS = {
|
|
|
969
1055
|
};
|
|
970
1056
|
|
|
971
1057
|
// packages/server/src/server-helpers/task-config.ts
|
|
972
|
-
import { existsSync as
|
|
1058
|
+
import { existsSync as existsSync5 } from "fs";
|
|
973
1059
|
async function readTaskConfig(projectRoot) {
|
|
974
1060
|
const taskConfigPath = resolveRigServerPaths(projectRoot).taskConfigPath;
|
|
975
|
-
if (!
|
|
1061
|
+
if (!existsSync5(taskConfigPath)) {
|
|
976
1062
|
return {};
|
|
977
1063
|
}
|
|
978
1064
|
try {
|
|
@@ -989,8 +1075,8 @@ var serverPathEnvQueue = Promise.resolve();
|
|
|
989
1075
|
async function withServerPathEnv(projectRoot, fn) {
|
|
990
1076
|
const waitForTurn = serverPathEnvQueue;
|
|
991
1077
|
let releaseTurn;
|
|
992
|
-
serverPathEnvQueue = new Promise((
|
|
993
|
-
releaseTurn =
|
|
1078
|
+
serverPathEnvQueue = new Promise((resolve9) => {
|
|
1079
|
+
releaseTurn = resolve9;
|
|
994
1080
|
});
|
|
995
1081
|
await waitForTurn;
|
|
996
1082
|
const paths = resolveServerAuthorityPaths(projectRoot);
|
|
@@ -1026,9 +1112,9 @@ async function withServerAuthorityEnvIfNeeded(projectRoot, fn) {
|
|
|
1026
1112
|
return withServerPathEnv(projectRoot, fn);
|
|
1027
1113
|
}
|
|
1028
1114
|
async function readWorkspaceTasks(projectRoot) {
|
|
1029
|
-
const issuesPath =
|
|
1115
|
+
const issuesPath = resolve8(resolveMonorepoRoot5(projectRoot), ".beads", "issues.jsonl");
|
|
1030
1116
|
const taskConfig = await readTaskConfig(projectRoot);
|
|
1031
|
-
if (!
|
|
1117
|
+
if (!existsSync6(issuesPath)) {
|
|
1032
1118
|
return [];
|
|
1033
1119
|
}
|
|
1034
1120
|
const latestById = new Map;
|
|
@@ -1074,7 +1160,7 @@ async function readWorkspaceTasks(projectRoot) {
|
|
|
1074
1160
|
if (false) {}
|
|
1075
1161
|
|
|
1076
1162
|
// packages/server/src/server-helpers/validation-failure.ts
|
|
1077
|
-
import { resolve as
|
|
1163
|
+
import { resolve as resolve9 } from "path";
|
|
1078
1164
|
import {
|
|
1079
1165
|
readJsonFile as readJsonFile4,
|
|
1080
1166
|
resolveTaskArtifactDirs
|
|
@@ -1088,7 +1174,7 @@ function summarizeRunValidationFailure(projectRoot, run) {
|
|
|
1088
1174
|
continue;
|
|
1089
1175
|
}
|
|
1090
1176
|
seen.add(artifactRoot);
|
|
1091
|
-
const summary = readJsonFile4(
|
|
1177
|
+
const summary = readJsonFile4(resolve9(artifactRoot, "validation-summary.json"), null);
|
|
1092
1178
|
if (!summary || summary.status !== "fail") {
|
|
1093
1179
|
continue;
|
|
1094
1180
|
}
|
|
@@ -1168,9 +1254,14 @@ function parseIssueRef(sourceTask, fallbackTaskId) {
|
|
|
1168
1254
|
return null;
|
|
1169
1255
|
return null;
|
|
1170
1256
|
}
|
|
1257
|
+
function githubProjectsEnabled(config) {
|
|
1258
|
+
const github = config?.github && typeof config.github === "object" && !Array.isArray(config.github) ? config.github : null;
|
|
1259
|
+
const projects = github?.projects && typeof github.projects === "object" && !Array.isArray(github.projects) ? github.projects : null;
|
|
1260
|
+
return projects?.enabled === true;
|
|
1261
|
+
}
|
|
1171
1262
|
async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config) {
|
|
1172
1263
|
if (!run.taskId)
|
|
1173
|
-
return;
|
|
1264
|
+
return false;
|
|
1174
1265
|
const issueNodeId = extractGitHubIssueNodeId(runSourceTaskIdentity(run));
|
|
1175
1266
|
try {
|
|
1176
1267
|
const result = await syncGitHubProjectStatusForTaskUpdate({
|
|
@@ -1181,28 +1272,86 @@ async function syncProjectStatusForRunLifecycle(projectRoot, run, status, config
|
|
|
1181
1272
|
config
|
|
1182
1273
|
});
|
|
1183
1274
|
if (!result.synced && result.reason !== "project-sync-disabled") {
|
|
1275
|
+
const detail = `Project status sync for ${run.taskId} could not run: ${result.reason}.`;
|
|
1184
1276
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
1185
1277
|
id: `log:${run.runId}:github-project-sync:${status}`,
|
|
1186
1278
|
title: "GitHub Project sync skipped",
|
|
1187
|
-
detail
|
|
1279
|
+
detail,
|
|
1188
1280
|
tone: "warn",
|
|
1189
1281
|
status: "running",
|
|
1190
1282
|
createdAt: new Date().toISOString(),
|
|
1191
1283
|
payload: { reason: result.reason, issueNodeId }
|
|
1192
1284
|
});
|
|
1285
|
+
if (githubProjectsEnabled(config)) {
|
|
1286
|
+
throw new Error(detail);
|
|
1287
|
+
}
|
|
1288
|
+
return false;
|
|
1193
1289
|
}
|
|
1290
|
+
return result.synced === true;
|
|
1194
1291
|
} catch (error) {
|
|
1292
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1195
1293
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
1196
1294
|
id: `log:${run.runId}:github-project-sync-error:${status}`,
|
|
1197
1295
|
title: "GitHub Project sync failed",
|
|
1198
|
-
detail
|
|
1296
|
+
detail,
|
|
1199
1297
|
tone: "error",
|
|
1200
1298
|
status: "running",
|
|
1201
1299
|
createdAt: new Date().toISOString(),
|
|
1202
1300
|
payload: { issueNodeId }
|
|
1203
1301
|
});
|
|
1302
|
+
if (githubProjectsEnabled(config)) {
|
|
1303
|
+
throw new Error(detail);
|
|
1304
|
+
}
|
|
1305
|
+
return false;
|
|
1204
1306
|
}
|
|
1205
1307
|
}
|
|
1308
|
+
function createCommandRunner(binary, extraEnv = {}) {
|
|
1309
|
+
return async (args, options) => {
|
|
1310
|
+
const child = spawn(binary, [...args], {
|
|
1311
|
+
cwd: options?.cwd,
|
|
1312
|
+
env: { ...process.env, ...extraEnv },
|
|
1313
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1314
|
+
});
|
|
1315
|
+
const stdoutChunks = [];
|
|
1316
|
+
const stderrChunks = [];
|
|
1317
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
1318
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
1319
|
+
const exitCode = await new Promise((resolve11) => {
|
|
1320
|
+
child.once("error", () => resolve11(1));
|
|
1321
|
+
child.once("close", (code) => resolve11(code ?? 1));
|
|
1322
|
+
});
|
|
1323
|
+
return {
|
|
1324
|
+
exitCode,
|
|
1325
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
1326
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8")
|
|
1327
|
+
};
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
function closeoutRecord(run) {
|
|
1331
|
+
const value = run.serverCloseout;
|
|
1332
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1333
|
+
}
|
|
1334
|
+
function closeoutPhasePatch(phase, status, extra = {}) {
|
|
1335
|
+
const updatedAt = new Date().toISOString();
|
|
1336
|
+
return {
|
|
1337
|
+
serverCloseout: {
|
|
1338
|
+
...extra,
|
|
1339
|
+
phase,
|
|
1340
|
+
status,
|
|
1341
|
+
updatedAt
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
function appendCloseoutStage(state, runId, phase, detail, status = "reviewing", tone = "info") {
|
|
1346
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1347
|
+
id: `log:${runId}:server-closeout:${phase}:${Date.now()}`,
|
|
1348
|
+
title: `Server closeout: ${phase}`,
|
|
1349
|
+
detail,
|
|
1350
|
+
tone,
|
|
1351
|
+
status,
|
|
1352
|
+
createdAt: new Date().toISOString()
|
|
1353
|
+
}, `server-closeout-${phase}`);
|
|
1354
|
+
}
|
|
1206
1355
|
async function autoAssignRunIssue(projectRoot, run) {
|
|
1207
1356
|
if (!run.taskId)
|
|
1208
1357
|
return;
|
|
@@ -1232,7 +1381,7 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1232
1381
|
return;
|
|
1233
1382
|
}
|
|
1234
1383
|
const config = await loadRigLifecycleConfig(projectRoot);
|
|
1235
|
-
await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
1384
|
+
const projectSynced = await syncProjectStatusForRunLifecycle(projectRoot, run, status, config);
|
|
1236
1385
|
if (status === "in_progress") {
|
|
1237
1386
|
await autoAssignRunIssue(projectRoot, run);
|
|
1238
1387
|
}
|
|
@@ -1248,24 +1397,53 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1248
1397
|
});
|
|
1249
1398
|
return;
|
|
1250
1399
|
}
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1400
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
1401
|
+
const previousStatus = normalizeString(sourceTask?.status) ?? normalizeString(sourceTask?.sourceStatus);
|
|
1402
|
+
const rollbackProjectSync = async () => {
|
|
1403
|
+
if (!projectSynced || !previousStatus || !run.taskId || !githubProjectsEnabled(config))
|
|
1404
|
+
return;
|
|
1405
|
+
await syncGitHubProjectStatusForTaskUpdate({
|
|
1406
|
+
taskId: run.taskId,
|
|
1407
|
+
status: previousStatus,
|
|
1408
|
+
issueNodeId: extractGitHubIssueNodeId(sourceTask),
|
|
1409
|
+
token: createGitHubAuthStore(projectRoot).readToken(),
|
|
1410
|
+
config
|
|
1411
|
+
}).catch((rollbackError) => {
|
|
1412
|
+
appendRunLogEntry(projectRoot, run.runId, {
|
|
1413
|
+
id: `log:${run.runId}:github-project-sync-rollback:${status}`,
|
|
1414
|
+
title: "GitHub Project sync rollback failed",
|
|
1415
|
+
detail: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
1416
|
+
tone: "error",
|
|
1417
|
+
status: "running",
|
|
1418
|
+
createdAt: new Date().toISOString()
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1422
|
+
let result;
|
|
1423
|
+
try {
|
|
1424
|
+
result = await updateConfiguredTaskSourceTask2(projectRoot, {
|
|
1425
|
+
taskId: run.taskId,
|
|
1426
|
+
sourceTask,
|
|
1427
|
+
update: {
|
|
1258
1428
|
status,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1429
|
+
comment: buildTaskRunLifecycleComment2({
|
|
1430
|
+
runId: run.runId,
|
|
1431
|
+
status,
|
|
1432
|
+
summary,
|
|
1433
|
+
runtimeWorkspace: normalizeString(run.worktreePath),
|
|
1434
|
+
logsDir: normalizeString(run.logRoot),
|
|
1435
|
+
sessionDir: normalizeString(run.sessionPath),
|
|
1436
|
+
errorText: options.errorText ?? normalizeString(run.errorText)
|
|
1437
|
+
})
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
await rollbackProjectSync();
|
|
1442
|
+
throw error;
|
|
1443
|
+
}
|
|
1267
1444
|
if (!result.updated) {
|
|
1268
1445
|
if (result.source === "plugin" || result.sourceKind) {
|
|
1446
|
+
await rollbackProjectSync();
|
|
1269
1447
|
throw new Error(`Configured task source${result.sourceKind ? ` (${result.sourceKind})` : ""} did not accept lifecycle update for ${result.taskId}.`);
|
|
1270
1448
|
}
|
|
1271
1449
|
appendRunLogEntry(projectRoot, run.runId, {
|
|
@@ -1278,6 +1456,277 @@ async function updateRunTaskSourceLifecycle(projectRoot, run, status, summary, o
|
|
|
1278
1456
|
});
|
|
1279
1457
|
}
|
|
1280
1458
|
}
|
|
1459
|
+
async function markServerOwnedCloseoutFailed(state, runId, error) {
|
|
1460
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1461
|
+
const current = readAuthorityRun9(state.projectRoot, runId);
|
|
1462
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1463
|
+
status: "failed",
|
|
1464
|
+
completedAt: new Date().toISOString(),
|
|
1465
|
+
errorText: detail,
|
|
1466
|
+
...closeoutPhasePatch("failed", "failed", { error: detail })
|
|
1467
|
+
});
|
|
1468
|
+
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1469
|
+
id: `log:${runId}:server-closeout-failed`,
|
|
1470
|
+
title: "Server-owned closeout failed",
|
|
1471
|
+
detail,
|
|
1472
|
+
tone: "error",
|
|
1473
|
+
status: "failed",
|
|
1474
|
+
createdAt: new Date().toISOString()
|
|
1475
|
+
}, "server-closeout-failed");
|
|
1476
|
+
if (current?.taskId) {
|
|
1477
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, { ...current, status: "failed", errorText: detail }, "failed", "Rig server-owned closeout failed.", { errorText: detail }).catch((sourceError) => {
|
|
1478
|
+
appendRunLogEntry(state.projectRoot, runId, {
|
|
1479
|
+
id: `log:${runId}:task-source-closeout-failed-update`,
|
|
1480
|
+
title: "Task source closeout failure update failed",
|
|
1481
|
+
detail: sourceError instanceof Error ? sourceError.message : String(sourceError),
|
|
1482
|
+
tone: "error",
|
|
1483
|
+
status: "failed",
|
|
1484
|
+
createdAt: new Date().toISOString()
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function scheduleServerOwnedPrCloseout(state, runId, reason) {
|
|
1490
|
+
const startedAt = new Date().toISOString();
|
|
1491
|
+
state.runProcesses.set(runId, {
|
|
1492
|
+
runId,
|
|
1493
|
+
child: null,
|
|
1494
|
+
startedAt,
|
|
1495
|
+
stopped: false
|
|
1496
|
+
});
|
|
1497
|
+
queueMicrotask(() => {
|
|
1498
|
+
withServerAuthorityEnvIfNeeded(state.projectRoot, async () => {
|
|
1499
|
+
try {
|
|
1500
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
await markServerOwnedCloseoutFailed(state, runId, error);
|
|
1503
|
+
} finally {
|
|
1504
|
+
state.runProcesses.delete(runId);
|
|
1505
|
+
broadcastSnapshotInvalidation(state, `server-closeout-${reason}-terminal`);
|
|
1506
|
+
await reconcileScheduler(state, `server-closeout-${reason}-terminal`);
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
async function runServerOwnedPrCloseout(state, runId) {
|
|
1512
|
+
const run = readAuthorityRun9(state.projectRoot, runId);
|
|
1513
|
+
if (!run)
|
|
1514
|
+
throw new Error(`Run not found: ${runId}`);
|
|
1515
|
+
const closeout = closeoutRecord(run);
|
|
1516
|
+
if (!closeout)
|
|
1517
|
+
return;
|
|
1518
|
+
const taskId = normalizeString(closeout.taskId) ?? normalizeString(run.taskId);
|
|
1519
|
+
if (!taskId)
|
|
1520
|
+
throw new Error("Server-owned closeout requires a task id.");
|
|
1521
|
+
const workspace = normalizeString(closeout.runtimeWorkspace) ?? normalizeString(run.worktreePath) ?? state.projectRoot;
|
|
1522
|
+
let branch = normalizeString(closeout.branch) ?? `rig/${taskId}-${runId}`;
|
|
1523
|
+
const config = await loadRigLifecycleConfig(state.projectRoot);
|
|
1524
|
+
const runPrMode = normalizeString(run.prMode);
|
|
1525
|
+
const prMode = runPrMode === "auto" || runPrMode === "ask" || runPrMode === "off" ? runPrMode : config?.pr?.mode ?? "off";
|
|
1526
|
+
const effectiveConfig = {
|
|
1527
|
+
...config ?? {},
|
|
1528
|
+
pr: {
|
|
1529
|
+
...config?.pr ?? {},
|
|
1530
|
+
mode: prMode,
|
|
1531
|
+
autoFixChecks: false,
|
|
1532
|
+
autoFixReview: false
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
const readCurrentRun = () => readAuthorityRun9(state.projectRoot, runId) ?? run;
|
|
1536
|
+
const sourceTask = runSourceTaskIdentity(run);
|
|
1537
|
+
const closeoutPhase = normalizeString(closeout.phase)?.toLowerCase() ?? "";
|
|
1538
|
+
const closeoutStatus = normalizeString(closeout.status)?.toLowerCase() ?? "";
|
|
1539
|
+
const closeoutPrUrl = normalizeString(closeout.prUrl);
|
|
1540
|
+
if (closeoutPhase === "completed" || closeoutStatus === "completed") {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (closeoutPhase === "close-source" && closeoutPrUrl) {
|
|
1544
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1545
|
+
status: "reviewing",
|
|
1546
|
+
...closeoutPhasePatch("close-source", "running", { ...closeout, prUrl: closeoutPrUrl, taskId, runtimeWorkspace: workspace, branch })
|
|
1547
|
+
});
|
|
1548
|
+
await closeIssueAfterMergedPr({
|
|
1549
|
+
projectRoot: state.projectRoot,
|
|
1550
|
+
taskId,
|
|
1551
|
+
runId,
|
|
1552
|
+
prUrl: closeoutPrUrl,
|
|
1553
|
+
sourceTask,
|
|
1554
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
1555
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
1556
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
const completedAt = new Date().toISOString();
|
|
1560
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1561
|
+
status: "completed",
|
|
1562
|
+
completedAt,
|
|
1563
|
+
errorText: null,
|
|
1564
|
+
...closeoutPhasePatch("completed", "completed", { ...closeout, prUrl: closeoutPrUrl, iterations: closeout.iterations, completedAt })
|
|
1565
|
+
});
|
|
1566
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${closeoutPrUrl}`, "completed", "info");
|
|
1567
|
+
emitRigEvent(state, {
|
|
1568
|
+
type: "rig.run.completed",
|
|
1569
|
+
aggregateId: runId,
|
|
1570
|
+
payload: { runId, taskId, prUrl: closeoutPrUrl, closeout: "merged" },
|
|
1571
|
+
createdAt: completedAt
|
|
1572
|
+
});
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
if (prMode === "off" || prMode === "ask") {
|
|
1576
|
+
const completedAt = new Date().toISOString();
|
|
1577
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1578
|
+
status: "completed",
|
|
1579
|
+
completedAt,
|
|
1580
|
+
errorText: null,
|
|
1581
|
+
...closeoutPhasePatch("completed", "completed", { taskId, runtimeWorkspace: workspace, branch, reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" })
|
|
1582
|
+
});
|
|
1583
|
+
appendCloseoutStage(state, runId, "completed", prMode === "ask" ? "Validation completed; PR creation awaits operator approval." : "Validation completed; PR automation disabled.", "completed", "info");
|
|
1584
|
+
emitRigEvent(state, {
|
|
1585
|
+
type: "rig.run.completed",
|
|
1586
|
+
aggregateId: runId,
|
|
1587
|
+
payload: { runId, taskId, closeout: "skipped", reason: prMode === "ask" ? "pr-mode-ask" : "pr-mode-off" },
|
|
1588
|
+
createdAt: completedAt
|
|
1589
|
+
});
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const githubToken = createGitHubAuthStore(state.projectRoot).readToken();
|
|
1593
|
+
const githubEnv = githubToken ? { RIG_GITHUB_TOKEN: githubToken, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken } : {};
|
|
1594
|
+
const gitCommand = createCommandRunner("git", githubEnv);
|
|
1595
|
+
const ghCommand = createCommandRunner("gh", githubEnv);
|
|
1596
|
+
const workspaceBranch = await gitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: workspace });
|
|
1597
|
+
const currentWorkspaceBranch = workspaceBranch.exitCode === 0 ? normalizeString(workspaceBranch.stdout) : null;
|
|
1598
|
+
if (currentWorkspaceBranch && currentWorkspaceBranch !== "HEAD" && currentWorkspaceBranch !== branch) {
|
|
1599
|
+
appendCloseoutStage(state, runId, "branch", `Using runtime workspace branch ${currentWorkspaceBranch} instead of recorded branch ${branch}.`, "reviewing", "info");
|
|
1600
|
+
branch = currentWorkspaceBranch;
|
|
1601
|
+
}
|
|
1602
|
+
const setCloseout = (phase, status, extra = {}) => {
|
|
1603
|
+
const previous = closeoutRecord(readCurrentRun()) ?? closeout;
|
|
1604
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1605
|
+
status: status === "failed" ? "failed" : status === "needs_attention" ? "needs_attention" : "reviewing",
|
|
1606
|
+
...closeoutPhasePatch(phase, status, { ...previous, ...extra })
|
|
1607
|
+
});
|
|
1608
|
+
};
|
|
1609
|
+
setCloseout("commit", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
1610
|
+
appendCloseoutStage(state, runId, "commit", `Committing changes in ${workspace}.`, "reviewing", "tool");
|
|
1611
|
+
const commit = await commitRunChanges({ cwd: workspace, message: `rig: complete task ${taskId}`, command: gitCommand });
|
|
1612
|
+
appendCloseoutStage(state, runId, "commit", commit.committed ? "Committed run workspace changes." : "No workspace changes to commit.", "reviewing", "tool");
|
|
1613
|
+
setCloseout("push", "running", { runtimeWorkspace: workspace, branch, taskId });
|
|
1614
|
+
const push = await gitCommand(["push", "--set-upstream", "origin", branch], { cwd: workspace });
|
|
1615
|
+
if (push.exitCode !== 0) {
|
|
1616
|
+
throw new Error(`git push --set-upstream origin ${branch} failed (${push.exitCode}): ${push.stderr ?? push.stdout ?? ""}`.trim());
|
|
1617
|
+
}
|
|
1618
|
+
const sourceTaskForPr = {
|
|
1619
|
+
title: normalizeString(sourceTask?.title) ?? normalizeString(run.title)
|
|
1620
|
+
};
|
|
1621
|
+
const artifactRoot = resolve10(state.projectRoot, "artifacts", taskId);
|
|
1622
|
+
setCloseout("pr-review-merge", "running", { runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1623
|
+
const pr = await runPrAutomation({
|
|
1624
|
+
projectRoot: workspace,
|
|
1625
|
+
taskId,
|
|
1626
|
+
runId,
|
|
1627
|
+
branch,
|
|
1628
|
+
config: effectiveConfig,
|
|
1629
|
+
sourceTask: sourceTaskForPr,
|
|
1630
|
+
artifactRoot,
|
|
1631
|
+
command: ghCommand,
|
|
1632
|
+
gitCommand,
|
|
1633
|
+
steerPi: async (message) => {
|
|
1634
|
+
appendCloseoutStage(state, runId, "feedback", message, "reviewing", "info");
|
|
1635
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
1636
|
+
id: `message:${runId}:server-closeout-feedback:${Date.now()}`,
|
|
1637
|
+
type: "user_message",
|
|
1638
|
+
text: message,
|
|
1639
|
+
createdAt: new Date().toISOString(),
|
|
1640
|
+
state: "completed"
|
|
1641
|
+
});
|
|
1642
|
+
},
|
|
1643
|
+
lifecycle: {
|
|
1644
|
+
onPrOpened: async ({ prUrl }) => {
|
|
1645
|
+
setCloseout("pr-opened", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1646
|
+
appendCloseoutStage(state, runId, "open-pr", prUrl, "reviewing", "tool");
|
|
1647
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "under_review", "Rig opened a pull request for this task.");
|
|
1648
|
+
},
|
|
1649
|
+
onReviewCiStarted: ({ prUrl, iteration }) => appendCloseoutStage(state, runId, "review-ci", `${prUrl} (iteration ${iteration})`, "reviewing", "info"),
|
|
1650
|
+
onFeedback: async ({ feedback }) => {
|
|
1651
|
+
appendCloseoutStage(state, runId, "feedback", feedback.join(`
|
|
1652
|
+
`), "reviewing", "error");
|
|
1653
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "ci_fixing", "Rig is fixing CI/review feedback for this task.");
|
|
1654
|
+
},
|
|
1655
|
+
onMergeStarted: async ({ prUrl }) => {
|
|
1656
|
+
setCloseout("merge", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot });
|
|
1657
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
1658
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "merging", "Rig is merging the pull request for this task.");
|
|
1659
|
+
},
|
|
1660
|
+
onMerged: ({ prUrl }) => {
|
|
1661
|
+
setCloseout("close-source", "running", { prUrl, runtimeWorkspace: workspace, branch, taskId, artifactRoot, merged: true });
|
|
1662
|
+
appendCloseoutStage(state, runId, "merge", prUrl, "reviewing", "tool");
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1666
|
+
if (pr.status === "merged" && pr.prUrl) {
|
|
1667
|
+
setCloseout("close-source", "running", { prUrl: pr.prUrl, iterations: pr.iterations });
|
|
1668
|
+
await closeIssueAfterMergedPr({
|
|
1669
|
+
projectRoot: state.projectRoot,
|
|
1670
|
+
taskId,
|
|
1671
|
+
runId,
|
|
1672
|
+
prUrl: pr.prUrl,
|
|
1673
|
+
sourceTask,
|
|
1674
|
+
updateTaskSource: async (projectRoot, input) => {
|
|
1675
|
+
await updateRunTaskSourceLifecycle(projectRoot, readCurrentRun(), "closed", "Rig merged the pull request and closed this task source.");
|
|
1676
|
+
return { updated: true, taskId: input.taskId, status: input.update.status, source: "server", sourceKind: "server" };
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
const completedAt = new Date().toISOString();
|
|
1680
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1681
|
+
status: "completed",
|
|
1682
|
+
completedAt,
|
|
1683
|
+
errorText: null,
|
|
1684
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations, completedAt })
|
|
1685
|
+
});
|
|
1686
|
+
appendCloseoutStage(state, runId, "completed", `PR merged and issue closed: ${pr.prUrl}`, "completed", "info");
|
|
1687
|
+
emitRigEvent(state, {
|
|
1688
|
+
type: "rig.run.completed",
|
|
1689
|
+
aggregateId: runId,
|
|
1690
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "merged" },
|
|
1691
|
+
createdAt: completedAt
|
|
1692
|
+
});
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (pr.status === "opened" && pr.prUrl) {
|
|
1696
|
+
const completedAt = new Date().toISOString();
|
|
1697
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1698
|
+
status: "completed",
|
|
1699
|
+
completedAt,
|
|
1700
|
+
errorText: null,
|
|
1701
|
+
...closeoutPhasePatch("completed", "completed", { prUrl: pr.prUrl, iterations: pr.iterations })
|
|
1702
|
+
});
|
|
1703
|
+
appendCloseoutStage(state, runId, "completed", `PR ready without merge: ${pr.prUrl}`, "completed", "info");
|
|
1704
|
+
emitRigEvent(state, {
|
|
1705
|
+
type: "rig.run.completed",
|
|
1706
|
+
aggregateId: runId,
|
|
1707
|
+
payload: { runId, taskId, prUrl: pr.prUrl, closeout: "pr-ready" },
|
|
1708
|
+
createdAt: completedAt
|
|
1709
|
+
});
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
const detail = pr.actionableFeedback.join(`
|
|
1713
|
+
`) || "PR automation did not merge the PR.";
|
|
1714
|
+
await updateRunTaskSourceLifecycle(state.projectRoot, readCurrentRun(), "needs_attention", "Rig needs operator attention before this task can proceed.", { errorText: detail }).catch((error) => {
|
|
1715
|
+
appendCloseoutStage(state, runId, "needs-attention-update", error instanceof Error ? error.message : String(error), "needs_attention", "error");
|
|
1716
|
+
});
|
|
1717
|
+
patchRunRecord(state.projectRoot, runId, {
|
|
1718
|
+
status: "needs_attention",
|
|
1719
|
+
completedAt: new Date().toISOString(),
|
|
1720
|
+
errorText: detail,
|
|
1721
|
+
...closeoutPhasePatch("needs_attention", "needs_attention", { feedback: pr.actionableFeedback, prUrl: pr.prUrl ?? null, iterations: pr.iterations })
|
|
1722
|
+
});
|
|
1723
|
+
appendCloseoutStage(state, runId, "needs-attention", detail, "needs_attention", "error");
|
|
1724
|
+
emitRigEvent(state, {
|
|
1725
|
+
type: "rig.run.needs-attention",
|
|
1726
|
+
aggregateId: runId,
|
|
1727
|
+
payload: { runId, taskId, error: detail, prUrl: pr.prUrl ?? null }
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1281
1730
|
var TERMINAL_RUN_STATUSES2 = new Set([
|
|
1282
1731
|
"completed",
|
|
1283
1732
|
"complete",
|
|
@@ -1303,11 +1752,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
1303
1752
|
return;
|
|
1304
1753
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
1305
1754
|
}
|
|
1755
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
1756
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
1757
|
+
if (fromReader)
|
|
1758
|
+
return fromReader;
|
|
1759
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
1760
|
+
if (projected)
|
|
1761
|
+
return projected;
|
|
1762
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
1763
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
1764
|
+
}
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1306
1767
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
1307
1768
|
if ("taskId" in input && input.taskId) {
|
|
1308
1769
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
1309
1770
|
}
|
|
1310
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
1771
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
1311
1772
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
1312
1773
|
const runDir = resolveAuthorityRunDir4(projectRoot, input.runId);
|
|
1313
1774
|
const runRecord = {
|
|
@@ -1341,11 +1802,11 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
|
|
|
1341
1802
|
initiatedBy: input.initiatedBy ?? null,
|
|
1342
1803
|
...sourceTask ? { sourceTask: sourceTaskContract(sourceTask) } : {}
|
|
1343
1804
|
};
|
|
1344
|
-
|
|
1345
|
-
|
|
1805
|
+
mkdirSync6(runDir, { recursive: true });
|
|
1806
|
+
writeFileSync6(resolve10(runDir, "run.json"), `${JSON.stringify(runRecord, null, 2)}
|
|
1346
1807
|
`, "utf8");
|
|
1347
1808
|
if ("initialPrompt" in input && input.initialPrompt && input.initialPrompt.trim().length > 0) {
|
|
1348
|
-
|
|
1809
|
+
writeFileSync6(resolve10(runDir, "timeline.jsonl"), `${JSON.stringify({
|
|
1349
1810
|
id: `message-${Date.now()}`,
|
|
1350
1811
|
type: "user_message",
|
|
1351
1812
|
text: input.initialPrompt,
|
|
@@ -1374,11 +1835,12 @@ async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTask
|
|
|
1374
1835
|
}
|
|
1375
1836
|
}
|
|
1376
1837
|
async function startLocalRun(state, runId, options) {
|
|
1377
|
-
const run =
|
|
1838
|
+
const run = readAuthorityRun9(state.projectRoot, runId);
|
|
1378
1839
|
if (!run) {
|
|
1379
1840
|
throw new Error(`Run not found: ${runId}`);
|
|
1380
1841
|
}
|
|
1381
1842
|
const startedAt = new Date().toISOString();
|
|
1843
|
+
const resumeMode = options?.resume === true;
|
|
1382
1844
|
state.runProcesses.set(runId, {
|
|
1383
1845
|
runId,
|
|
1384
1846
|
child: null,
|
|
@@ -1395,9 +1857,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
1395
1857
|
summary: run.title
|
|
1396
1858
|
});
|
|
1397
1859
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
1398
|
-
id: `log:${runId}:prepare`,
|
|
1399
|
-
title: "Rig task run starting",
|
|
1400
|
-
detail: run.taskId ?? run.title,
|
|
1860
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
1861
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
1862
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
1401
1863
|
tone: "info",
|
|
1402
1864
|
status: "preparing",
|
|
1403
1865
|
createdAt: startedAt
|
|
@@ -1405,8 +1867,8 @@ async function startLocalRun(state, runId, options) {
|
|
|
1405
1867
|
broadcastRunLogAppended(state, runId, readLatestRawRunLog(state.projectRoot, runId));
|
|
1406
1868
|
broadcastSnapshotInvalidation(state);
|
|
1407
1869
|
const cliProjectRoot = resolveLocalRunCliProjectRoot(state.projectRoot);
|
|
1408
|
-
const cliEntryPoint =
|
|
1409
|
-
if (!
|
|
1870
|
+
const cliEntryPoint = resolve10(cliProjectRoot, "packages/cli/bin/rig.ts");
|
|
1871
|
+
if (!existsSync7(cliEntryPoint)) {
|
|
1410
1872
|
const completedAt = new Date().toISOString();
|
|
1411
1873
|
const failureSummary = `Rig task-run entrypoint missing at ${relative2(state.projectRoot, cliEntryPoint)}`;
|
|
1412
1874
|
patchRunRecord(state.projectRoot, runId, {
|
|
@@ -1473,12 +1935,17 @@ async function startLocalRun(state, runId, options) {
|
|
|
1473
1935
|
RIG_HOST_PROJECT_ROOT: cliProjectRoot,
|
|
1474
1936
|
RIG_RUNTIME_BASE_REF: process.env.RIG_RUNTIME_BASE_REF ?? "HEAD",
|
|
1475
1937
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
1938
|
+
RIG_SERVER_OWNS_CLOSEOUT: "1",
|
|
1476
1939
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
1477
1940
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
1478
1941
|
...bridgeGitHubToken ? {
|
|
1479
1942
|
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
1480
1943
|
GITHUB_TOKEN: bridgeGitHubToken,
|
|
1481
1944
|
GH_TOKEN: bridgeGitHubToken
|
|
1945
|
+
} : {},
|
|
1946
|
+
...resumeMode ? {
|
|
1947
|
+
RIG_RUN_RESUME: "1",
|
|
1948
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
1482
1949
|
} : {}
|
|
1483
1950
|
},
|
|
1484
1951
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1502,6 +1969,25 @@ async function startLocalRun(state, runId, options) {
|
|
|
1502
1969
|
broadcastSnapshotInvalidation(state);
|
|
1503
1970
|
continue;
|
|
1504
1971
|
}
|
|
1972
|
+
if (line.startsWith("__RIG_WRAPPER_EVENT__")) {
|
|
1973
|
+
try {
|
|
1974
|
+
const wrapperEvent = JSON.parse(line.slice("__RIG_WRAPPER_EVENT__".length));
|
|
1975
|
+
const eventType = normalizeString(wrapperEvent.type);
|
|
1976
|
+
const payload = wrapperEvent.payload && typeof wrapperEvent.payload === "object" && !Array.isArray(wrapperEvent.payload) ? wrapperEvent.payload : {};
|
|
1977
|
+
if (eventType === "pi.session.ready" && payload.privateMetadata && typeof payload.privateMetadata === "object" && !Array.isArray(payload.privateMetadata)) {
|
|
1978
|
+
patchRunPiSessionMetadata(state.projectRoot, runId, payload.privateMetadata);
|
|
1979
|
+
}
|
|
1980
|
+
appendRunTimelineEntry(state.projectRoot, runId, {
|
|
1981
|
+
id: `timeline:${runId}:${Date.now()}:wrapper:${eventType ?? "event"}`,
|
|
1982
|
+
type: "wrapper-event",
|
|
1983
|
+
eventType,
|
|
1984
|
+
payload,
|
|
1985
|
+
createdAt: normalizeString(wrapperEvent.at) ?? new Date().toISOString()
|
|
1986
|
+
});
|
|
1987
|
+
} catch {}
|
|
1988
|
+
broadcastSnapshotInvalidation(state, "wrapper-event");
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1505
1991
|
appendRunLogEntryAndBroadcast(state, runId, {
|
|
1506
1992
|
id: `log:${runId}:${Date.now()}`,
|
|
1507
1993
|
title,
|
|
@@ -1519,18 +2005,24 @@ async function startLocalRun(state, runId, options) {
|
|
|
1519
2005
|
handleRunProcessOutput(Buffer.isBuffer(data) ? data.toString("utf8") : String(data), "error", "Rig task run stderr");
|
|
1520
2006
|
});
|
|
1521
2007
|
try {
|
|
1522
|
-
const exit = await new Promise((
|
|
1523
|
-
child.once("error", (error) =>
|
|
1524
|
-
child.once("close", (code, signal) =>
|
|
2008
|
+
const exit = await new Promise((resolve11) => {
|
|
2009
|
+
child.once("error", (error) => resolve11({ code: 1, signal: null, error }));
|
|
2010
|
+
child.once("close", (code, signal) => resolve11({ code, signal }));
|
|
1525
2011
|
});
|
|
1526
2012
|
if (exit.error) {
|
|
1527
2013
|
throw new Error(`Failed to start task run: ${exit.error.message}`);
|
|
1528
2014
|
}
|
|
1529
|
-
const current =
|
|
2015
|
+
const current = readAuthorityRun9(state.projectRoot, runId);
|
|
1530
2016
|
if (!current) {
|
|
1531
2017
|
return;
|
|
1532
2018
|
}
|
|
1533
|
-
if (
|
|
2019
|
+
if (closeoutRecord(current)?.status === "pending") {
|
|
2020
|
+
try {
|
|
2021
|
+
await runServerOwnedPrCloseout(state, runId);
|
|
2022
|
+
} catch (closeoutError) {
|
|
2023
|
+
await markServerOwnedCloseoutFailed(state, runId, closeoutError);
|
|
2024
|
+
}
|
|
2025
|
+
} else if (exit.code !== 0 && current.status !== "completed" && current.status !== "stopped") {
|
|
1534
2026
|
const completedAt = current.completedAt ?? new Date().toISOString();
|
|
1535
2027
|
const failureSummary = normalizeString(current.errorText) ?? summarizeRunValidationFailure(state.projectRoot, current) ?? `Rig task-run exited with code ${String(exit.code ?? "unknown")}`;
|
|
1536
2028
|
if (current.status !== "failed") {
|
|
@@ -1621,24 +2113,24 @@ function resolveLocalRunCliProjectRoot(projectRoot) {
|
|
|
1621
2113
|
process.env.PROJECT_RIG_ROOT?.trim()
|
|
1622
2114
|
].filter((value) => !!value);
|
|
1623
2115
|
for (const candidate of envCandidates) {
|
|
1624
|
-
if (
|
|
1625
|
-
return
|
|
2116
|
+
if (existsSync7(resolve10(candidate, "packages/cli/bin/rig.ts"))) {
|
|
2117
|
+
return resolve10(candidate);
|
|
1626
2118
|
}
|
|
1627
2119
|
}
|
|
1628
|
-
if (
|
|
2120
|
+
if (existsSync7(resolve10(projectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1629
2121
|
return projectRoot;
|
|
1630
2122
|
}
|
|
1631
2123
|
try {
|
|
1632
2124
|
const monorepoRoot = resolveMonorepoRoot6(projectRoot);
|
|
1633
|
-
const outerProjectRoot =
|
|
1634
|
-
if (
|
|
2125
|
+
const outerProjectRoot = dirname6(dirname6(monorepoRoot));
|
|
2126
|
+
if (existsSync7(resolve10(outerProjectRoot, "packages/cli/bin/rig.ts"))) {
|
|
1635
2127
|
return outerProjectRoot;
|
|
1636
2128
|
}
|
|
1637
2129
|
} catch {}
|
|
1638
2130
|
return projectRoot;
|
|
1639
2131
|
}
|
|
1640
2132
|
async function resumeRunRecord(state, input) {
|
|
1641
|
-
const run =
|
|
2133
|
+
const run = readAuthorityRun9(state.projectRoot, input.runId);
|
|
1642
2134
|
if (!run) {
|
|
1643
2135
|
throw new Error(`Run not found: ${input.runId}`);
|
|
1644
2136
|
}
|
|
@@ -1651,15 +2143,27 @@ async function resumeRunRecord(state, input) {
|
|
|
1651
2143
|
if (run.status === "completed") {
|
|
1652
2144
|
throw new Error("Completed runs cannot be resumed.");
|
|
1653
2145
|
}
|
|
1654
|
-
|
|
2146
|
+
const closeout = closeoutRecord(run);
|
|
2147
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
2148
|
+
if (EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus)) {
|
|
2149
|
+
patchRunRecord(state.projectRoot, input.runId, {
|
|
2150
|
+
status: "reviewing",
|
|
2151
|
+
completedAt: null,
|
|
2152
|
+
errorText: null,
|
|
2153
|
+
...closeoutPhasePatch("queued", "pending", { ...closeout, resumedAt: input.createdAt })
|
|
2154
|
+
});
|
|
2155
|
+
scheduleServerOwnedPrCloseout(state, input.runId, "explicit-resume");
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
1655
2159
|
}
|
|
1656
2160
|
function appendRunMessage(projectRoot, input) {
|
|
1657
|
-
const run =
|
|
2161
|
+
const run = readAuthorityRun9(projectRoot, input.runId);
|
|
1658
2162
|
if (!run) {
|
|
1659
2163
|
throw new Error(`Run not found: ${input.runId}`);
|
|
1660
2164
|
}
|
|
1661
|
-
const timelinePath =
|
|
1662
|
-
const existingLines = fileExists(timelinePath) ?
|
|
2165
|
+
const timelinePath = resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "timeline.jsonl");
|
|
2166
|
+
const existingLines = fileExists(timelinePath) ? readFileSync4(timelinePath, "utf8").trim() : "";
|
|
1663
2167
|
const nextLine = JSON.stringify({
|
|
1664
2168
|
id: input.messageId,
|
|
1665
2169
|
type: "user_message",
|
|
@@ -1667,18 +2171,18 @@ function appendRunMessage(projectRoot, input) {
|
|
|
1667
2171
|
attachments: input.attachments ?? [],
|
|
1668
2172
|
createdAt: input.createdAt
|
|
1669
2173
|
});
|
|
1670
|
-
|
|
2174
|
+
writeFileSync6(timelinePath, existingLines.length > 0 ? `${existingLines}
|
|
1671
2175
|
${nextLine}
|
|
1672
2176
|
` : `${nextLine}
|
|
1673
2177
|
`, "utf8");
|
|
1674
|
-
writeJsonFile4(
|
|
2178
|
+
writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), {
|
|
1675
2179
|
...run,
|
|
1676
2180
|
updatedAt: input.createdAt
|
|
1677
2181
|
});
|
|
1678
2182
|
}
|
|
1679
2183
|
async function stopRunRecord(stateOrProjectRoot, input) {
|
|
1680
2184
|
const projectRoot = typeof stateOrProjectRoot === "string" ? stateOrProjectRoot : stateOrProjectRoot.projectRoot;
|
|
1681
|
-
const run =
|
|
2185
|
+
const run = readAuthorityRun9(projectRoot, input.runId);
|
|
1682
2186
|
if (!run) {
|
|
1683
2187
|
throw new Error(`Run not found: ${input.runId}`);
|
|
1684
2188
|
}
|
|
@@ -1697,7 +2201,7 @@ async function stopRunRecord(stateOrProjectRoot, input) {
|
|
|
1697
2201
|
completedAt: run.completedAt ?? input.createdAt,
|
|
1698
2202
|
updatedAt: input.createdAt
|
|
1699
2203
|
};
|
|
1700
|
-
writeJsonFile4(
|
|
2204
|
+
writeJsonFile4(resolve10(resolveAuthorityRunDir4(projectRoot, input.runId), "run.json"), nextRun);
|
|
1701
2205
|
if (run.status !== "completed" && run.taskId) {
|
|
1702
2206
|
const taskId = run.taskId;
|
|
1703
2207
|
(async () => {
|
|
@@ -1735,34 +2239,63 @@ function removeTaskIdsFromQueueState2(projectRoot, taskIds) {
|
|
|
1735
2239
|
writeQueueState(projectRoot, next);
|
|
1736
2240
|
return next;
|
|
1737
2241
|
}
|
|
1738
|
-
var
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2242
|
+
var RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running"]);
|
|
2243
|
+
var EXPLICIT_RESUMABLE_SERVER_CLOSEOUT_STATUSES = new Set(["pending", "running", "needs_attention"]);
|
|
2244
|
+
var ACTIVE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
2245
|
+
function processExists(pid) {
|
|
2246
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
2247
|
+
return false;
|
|
2248
|
+
try {
|
|
2249
|
+
process.kill(pid, 0);
|
|
2250
|
+
return true;
|
|
2251
|
+
} catch {
|
|
2252
|
+
return false;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
function recoverStaleLocalRun(projectRoot, run) {
|
|
2256
|
+
const record = run;
|
|
2257
|
+
if (run.mode !== "local")
|
|
2258
|
+
return false;
|
|
2259
|
+
const closeout = closeoutRecord(record);
|
|
2260
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
2261
|
+
const status = normalizeString(record.status)?.toLowerCase() ?? "";
|
|
2262
|
+
if (RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus))
|
|
2263
|
+
return false;
|
|
2264
|
+
if (closeoutStatus === "needs_attention") {
|
|
2265
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
2266
|
+
return false;
|
|
2267
|
+
const completedAt2 = record.completedAt ?? new Date().toISOString();
|
|
2268
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
2269
|
+
status: "needs_attention",
|
|
2270
|
+
completedAt: completedAt2,
|
|
2271
|
+
errorText: normalizeString(record.errorText) ?? (Array.isArray(closeout?.feedback) ? closeout.feedback.map(String).join(`
|
|
2272
|
+
`) : null)
|
|
1762
2273
|
});
|
|
1763
|
-
|
|
2274
|
+
return true;
|
|
1764
2275
|
}
|
|
1765
|
-
|
|
2276
|
+
if (!ACTIVE_LOCAL_RUN_STATUSES.has(status))
|
|
2277
|
+
return false;
|
|
2278
|
+
const serverPid = typeof record.serverPid === "number" ? record.serverPid : null;
|
|
2279
|
+
const childPid = typeof record.pid === "number" ? record.pid : null;
|
|
2280
|
+
if (serverPid === null && childPid === null)
|
|
2281
|
+
return false;
|
|
2282
|
+
const hasLiveRecordedProcess = [serverPid, childPid].some((pid) => typeof pid === "number" && processExists(pid));
|
|
2283
|
+
if (hasLiveRecordedProcess && serverPid === process.pid)
|
|
2284
|
+
return false;
|
|
2285
|
+
const completedAt = new Date().toISOString();
|
|
2286
|
+
patchRunRecord(projectRoot, run.runId, {
|
|
2287
|
+
status: "failed",
|
|
2288
|
+
completedAt,
|
|
2289
|
+
errorText: `Recovered stale local run ${run.runId} after server startup; no active server-owned process was tracking it.`
|
|
2290
|
+
});
|
|
2291
|
+
return true;
|
|
2292
|
+
}
|
|
2293
|
+
function collectResumableServerCloseouts(state, runs) {
|
|
2294
|
+
return runs.filter((run) => {
|
|
2295
|
+
const closeout = closeoutRecord(run);
|
|
2296
|
+
const closeoutStatus = normalizeString(closeout?.status)?.toLowerCase() ?? "";
|
|
2297
|
+
return run.mode === "local" && RESUMABLE_SERVER_CLOSEOUT_STATUSES.has(closeoutStatus) && !state.runProcesses.has(run.runId);
|
|
2298
|
+
});
|
|
1766
2299
|
}
|
|
1767
2300
|
async function reconcileScheduler(state, reason) {
|
|
1768
2301
|
if (state.scheduler.reconciling) {
|
|
@@ -1777,7 +2310,28 @@ async function reconcileScheduler(state, reason) {
|
|
|
1777
2310
|
const queue = readQueueState(state.projectRoot);
|
|
1778
2311
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
1779
2312
|
let runs = listAuthorityRuns7(state.projectRoot);
|
|
1780
|
-
let changed =
|
|
2313
|
+
let changed = false;
|
|
2314
|
+
for (const run of runs) {
|
|
2315
|
+
if (!state.runProcesses.has(run.runId) && recoverStaleLocalRun(state.projectRoot, run)) {
|
|
2316
|
+
changed = true;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
if (changed) {
|
|
2320
|
+
runs = listAuthorityRuns7(state.projectRoot);
|
|
2321
|
+
}
|
|
2322
|
+
const resumableCloseouts = collectResumableServerCloseouts(state, runs);
|
|
2323
|
+
for (const run of resumableCloseouts) {
|
|
2324
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
2325
|
+
id: `log:${run.runId}:server-closeout-auto-resume:${Date.now()}`,
|
|
2326
|
+
title: "Server-owned closeout auto-resume scheduled",
|
|
2327
|
+
detail: `Rig server recovered closeout checkpoint ${run.runId} after ${reason}; resuming the server-owned lifecycle phase.`,
|
|
2328
|
+
tone: "info",
|
|
2329
|
+
status: "reviewing",
|
|
2330
|
+
createdAt: new Date().toISOString()
|
|
2331
|
+
});
|
|
2332
|
+
scheduleServerOwnedPrCloseout(state, run.runId, "auto-resume");
|
|
2333
|
+
changed = true;
|
|
2334
|
+
}
|
|
1781
2335
|
if (changed) {
|
|
1782
2336
|
runs = listAuthorityRuns7(state.projectRoot);
|
|
1783
2337
|
}
|
|
@@ -1850,8 +2404,10 @@ async function reconcileScheduler(state, reason) {
|
|
|
1850
2404
|
}
|
|
1851
2405
|
}
|
|
1852
2406
|
export {
|
|
2407
|
+
updateRunTaskSourceLifecycle,
|
|
1853
2408
|
stopRunRecord,
|
|
1854
2409
|
startLocalRun,
|
|
2410
|
+
runServerOwnedPrCloseout,
|
|
1855
2411
|
resumeRunRecord,
|
|
1856
2412
|
resolveLocalRunCliProjectRoot,
|
|
1857
2413
|
removeTaskIdsFromQueueState2 as removeTaskIdsFromQueueState,
|