omp-conductor 0.20.1 → 0.20.2
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/package.json +1 -1
- package/schema/config.schema.json +24 -0
- package/src/commands/companion.ts +52 -16
- package/src/commands/drain.ts +12 -5
- package/src/commands/worker.ts +5 -0
- package/src/config-schema.ts +8 -0
- package/src/config.ts +17 -2
- package/src/daemon/drain.ts +33 -0
- package/src/daemon/http.ts +28 -0
- package/src/daemon/review.ts +11 -11
- package/src/daemon/supervision.ts +144 -1
- package/src/daemon/tick.ts +31 -14
- package/src/daemon/views.ts +17 -0
- package/src/daemon.ts +2 -1
- package/src/decisions.ts +20 -9
- package/src/diff-flags.ts +93 -22
- package/src/doctor.ts +25 -7
- package/src/escalate.ts +187 -15
- package/src/failure-class.ts +176 -4
- package/src/fleet.ts +42 -5
- package/src/graph-health.ts +17 -1
- package/src/groom.ts +11 -0
- package/src/orchestrator-tick.ts +8 -2
- package/src/reports.ts +4 -1
- package/src/settlement.ts +98 -7
- package/src/status-render.ts +27 -2
- package/src/store.ts +50 -13
- package/src/to-spec.ts +52 -0
- package/src/tracker/github.ts +132 -10
- package/src/types.ts +31 -0
- package/src/verbs/server.ts +16 -12
package/src/tracker/github.ts
CHANGED
|
@@ -94,6 +94,12 @@ interface GhCheck {
|
|
|
94
94
|
status?: string;
|
|
95
95
|
conclusion?: string;
|
|
96
96
|
state?: string;
|
|
97
|
+
/** When the run started (`startedAt` on a GraphQL CheckRun, `started_at`
|
|
98
|
+
* over REST); a StatusContext stamps `createdAt` instead. Undefined when
|
|
99
|
+
* the payload spelled none — unordered entries resolve pessimistically
|
|
100
|
+
* (#1066), never invented. */
|
|
101
|
+
startedAt?: string;
|
|
102
|
+
createdAt?: string;
|
|
97
103
|
detailsUrl?: string;
|
|
98
104
|
}
|
|
99
105
|
|
|
@@ -419,6 +425,75 @@ function checkVerdict(check: GhCheck): CheckVerdict {
|
|
|
419
425
|
return "failed";
|
|
420
426
|
}
|
|
421
427
|
|
|
428
|
+
/** One check normalized onto the verdict vocabulary, carrying whatever run
|
|
429
|
+
* timestamp its payload spelled: undefined means unordered, which
|
|
430
|
+
* {@link resolveNewestPerName} treats pessimistically. */
|
|
431
|
+
interface NamedVerdict {
|
|
432
|
+
readonly name: string;
|
|
433
|
+
readonly verdict: CheckVerdict;
|
|
434
|
+
readonly state: string;
|
|
435
|
+
readonly startedAt: string | undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Epoch ms of an ISO stamp, or undefined when absent or unreadable — both
|
|
439
|
+
* leave the entry unordered rather than inventing an age for it. */
|
|
440
|
+
function startedAtMs(startedAt: string | undefined): number | undefined {
|
|
441
|
+
if (startedAt === undefined) return undefined;
|
|
442
|
+
const ms = Date.parse(startedAt);
|
|
443
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Worse beats better when a name's deciding entries disagree. */
|
|
447
|
+
const VERDICT_RANK: Readonly<Record<CheckVerdict, number>> = { failed: 0, pending: 1, green: 2 };
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Decide every check name by its newest run, never by payload order (#1066).
|
|
451
|
+
*
|
|
452
|
+
* One head can carry several check-runs of one name — a re-run creates a new
|
|
453
|
+
* run record without retiring the old one — and folding them first-match lets
|
|
454
|
+
* a stale `skipped` mask a newer `failure`: exactly how veltro#717 nearly
|
|
455
|
+
* merged green over its only upgrade-path check, whose failing run started
|
|
456
|
+
* five minutes behind the skipped row of the same name. Entries are grouped
|
|
457
|
+
* by name and each group is decided by its newest tier, GitHub's own checks-UI
|
|
458
|
+
* rule:
|
|
459
|
+
*
|
|
460
|
+
* - timed entries at the maximum `started_at` decide; older runs of the name
|
|
461
|
+
* cannot mask them, so a re-run that went green supersedes its red
|
|
462
|
+
* predecessor;
|
|
463
|
+
* - ties at one instant resolve pessimistically — a failure anywhere in the
|
|
464
|
+
* deciding tier fails the name;
|
|
465
|
+
* - an entry without a readable timestamp cannot be proven older than
|
|
466
|
+
* anything, so it belongs to every deciding tier (pessimistic again).
|
|
467
|
+
*
|
|
468
|
+
* The representative kept per name is the deciding entry itself, so the
|
|
469
|
+
* folds' refusal lines render the decided verdict's own spelling.
|
|
470
|
+
*/
|
|
471
|
+
function resolveNewestPerName(entries: readonly NamedVerdict[]): NamedVerdict[] {
|
|
472
|
+
const groups = new Map<string, { entry: NamedVerdict; startedMs: number | undefined }[]>();
|
|
473
|
+
for (const entry of entries) {
|
|
474
|
+
const stamped = { entry, startedMs: startedAtMs(entry.startedAt) };
|
|
475
|
+
const group = groups.get(entry.name);
|
|
476
|
+
if (group === undefined) groups.set(entry.name, [stamped]);
|
|
477
|
+
else group.push(stamped);
|
|
478
|
+
}
|
|
479
|
+
const resolved: NamedVerdict[] = [];
|
|
480
|
+
for (const group of groups.values()) {
|
|
481
|
+
let newest = -Infinity;
|
|
482
|
+
for (const { startedMs } of group) {
|
|
483
|
+
if (startedMs !== undefined && startedMs > newest) newest = startedMs;
|
|
484
|
+
}
|
|
485
|
+
let decided: NamedVerdict | undefined;
|
|
486
|
+
for (const { entry, startedMs } of group) {
|
|
487
|
+
if (startedMs !== undefined && startedMs !== newest) continue;
|
|
488
|
+
if (decided === undefined || VERDICT_RANK[entry.verdict] < VERDICT_RANK[decided.verdict]) {
|
|
489
|
+
decided = entry;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (decided !== undefined) resolved.push(decided);
|
|
493
|
+
}
|
|
494
|
+
return resolved;
|
|
495
|
+
}
|
|
496
|
+
|
|
422
497
|
/**
|
|
423
498
|
* Verify one `gh pr view --json state,isDraft,headRefOid,statusCheckRollup`
|
|
424
499
|
* payload, optionally against a caller-observed head. A missing rollup is
|
|
@@ -430,6 +505,10 @@ function checkVerdict(check: GhCheck): CheckVerdict {
|
|
|
430
505
|
* a different question: *what is this PR's state*, and a merged or closed PR
|
|
431
506
|
* is reported as a fact rather than a failure — the merge gate paths never
|
|
432
507
|
* pass `read`, so the `expected OPEN` refusal stays exactly where it gates.
|
|
508
|
+
*
|
|
509
|
+
* Same-name entries are decided by recency, not payload order (#1066): only
|
|
510
|
+
* the newest run of a name answers for it, ties and unreadable stamps resolve
|
|
511
|
+
* pessimistically, and the green count names de-duplicated checks.
|
|
433
512
|
*/
|
|
434
513
|
export function prVerificationFrom(
|
|
435
514
|
raw: string,
|
|
@@ -467,23 +546,34 @@ export function prVerificationFrom(
|
|
|
467
546
|
if (checks.length === 0) {
|
|
468
547
|
return { status: "pending", reason: "GitHub has not reported any checks yet", headSha };
|
|
469
548
|
}
|
|
470
|
-
|
|
549
|
+
// Same-name runs decide by recency, not payload order (#1066): the rollup
|
|
550
|
+
// can carry an older run of a name beside a newer one, and first-match
|
|
551
|
+
// folding would let the older verdict answer for the name.
|
|
552
|
+
const resolved = resolveNewestPerName(
|
|
553
|
+
checks.map((check): NamedVerdict => ({
|
|
554
|
+
name: checkName(check),
|
|
555
|
+
verdict: checkVerdict(check),
|
|
556
|
+
state: check.conclusion ?? check.state ?? "unknown",
|
|
557
|
+
startedAt: check.startedAt ?? check.createdAt,
|
|
558
|
+
})),
|
|
559
|
+
);
|
|
560
|
+
const failed = resolved.filter((check) => check.verdict === "failed");
|
|
471
561
|
if (failed.length > 0) {
|
|
472
562
|
return {
|
|
473
563
|
status: "failed",
|
|
474
|
-
reason: `Checks failed: ${failed.map((check) => `${
|
|
564
|
+
reason: `Checks failed: ${failed.map((check) => `${check.name} (${check.state})`).join(", ")}`,
|
|
475
565
|
headSha,
|
|
476
566
|
};
|
|
477
567
|
}
|
|
478
|
-
const pending =
|
|
568
|
+
const pending = resolved.filter((check) => check.verdict === "pending");
|
|
479
569
|
if (pending.length > 0) {
|
|
480
570
|
return {
|
|
481
571
|
status: "pending",
|
|
482
|
-
reason: `Checks pending: ${pending.map(
|
|
572
|
+
reason: `Checks pending: ${pending.map((check) => check.name).join(", ")}`,
|
|
483
573
|
headSha,
|
|
484
574
|
};
|
|
485
575
|
}
|
|
486
|
-
return { status: "green", reason: `${
|
|
576
|
+
return { status: "green", reason: `${resolved.length} checks succeeded or were skipped`, headSha };
|
|
487
577
|
}
|
|
488
578
|
|
|
489
579
|
/**
|
|
@@ -538,6 +628,9 @@ interface RestCheckConclusion {
|
|
|
538
628
|
* GitHub spelled it — surfaced in refusals exactly like the GraphQL one. */
|
|
539
629
|
state: string;
|
|
540
630
|
verdict: CheckVerdict;
|
|
631
|
+
/** The run's `started_at` (or a commit status's `updated_at`), undefined
|
|
632
|
+
* when the page spelled none — unordered, never invented (#1066). */
|
|
633
|
+
startedAt: string | undefined;
|
|
541
634
|
}
|
|
542
635
|
|
|
543
636
|
/** One REST check plane, parsed strictly. */
|
|
@@ -579,6 +672,7 @@ function restRunConclusionFrom(row: { readonly [key: string]: unknown }): RestCh
|
|
|
579
672
|
const name = row["name"];
|
|
580
673
|
const status = row["status"];
|
|
581
674
|
const conclusion = row["conclusion"];
|
|
675
|
+
const startedAt = row["started_at"];
|
|
582
676
|
if (typeof name !== "string" || name === "") return undefined;
|
|
583
677
|
if (typeof status !== "string") return undefined;
|
|
584
678
|
if (conclusion !== undefined && conclusion !== null && typeof conclusion !== "string") return undefined;
|
|
@@ -591,7 +685,12 @@ function restRunConclusionFrom(row: { readonly [key: string]: unknown }): RestCh
|
|
|
591
685
|
: conclusion === "success" || conclusion === "skipped" || conclusion === "neutral"
|
|
592
686
|
? "green"
|
|
593
687
|
: "failed";
|
|
594
|
-
return {
|
|
688
|
+
return {
|
|
689
|
+
name,
|
|
690
|
+
state: typeof conclusion === "string" ? conclusion : status,
|
|
691
|
+
verdict,
|
|
692
|
+
startedAt: typeof startedAt === "string" ? startedAt : undefined,
|
|
693
|
+
};
|
|
595
694
|
}
|
|
596
695
|
|
|
597
696
|
/** One REST commit-status member onto the rollup vocabulary, or undefined when
|
|
@@ -599,12 +698,14 @@ function restRunConclusionFrom(row: { readonly [key: string]: unknown }): RestCh
|
|
|
599
698
|
function restStatusConclusionFrom(row: { readonly [key: string]: unknown }): RestCheckConclusion | undefined {
|
|
600
699
|
const context = row["context"];
|
|
601
700
|
const state = row["state"];
|
|
701
|
+
const updatedAt = typeof row["updated_at"] === "string" ? row["updated_at"] : undefined;
|
|
702
|
+
const createdAt = typeof row["created_at"] === "string" ? row["created_at"] : undefined;
|
|
602
703
|
if (typeof context !== "string" || context === "") return undefined;
|
|
603
704
|
if (typeof state !== "string") return undefined;
|
|
604
705
|
// StatusContext semantics: `success` is green, `pending` pending, anything
|
|
605
706
|
// else (`failure`/`error`) is failed.
|
|
606
707
|
const verdict: CheckVerdict = state === "success" ? "green" : state === "pending" ? "pending" : "failed";
|
|
607
|
-
return { name: context, state, verdict };
|
|
708
|
+
return { name: context, state, verdict, startedAt: updatedAt ?? createdAt };
|
|
608
709
|
}
|
|
609
710
|
|
|
610
711
|
/**
|
|
@@ -706,6 +807,10 @@ function restProjectionVerdict(
|
|
|
706
807
|
* Throws on a payload that cannot prove the head or either check plane, so the
|
|
707
808
|
* caller maps it to `head-unresolvable` exactly as it does for the GraphQL
|
|
708
809
|
* parser.
|
|
810
|
+
*
|
|
811
|
+
* Same-name runs are decided by recency, not page order (#1066): the newest
|
|
812
|
+
* attempt of a name answers for it, so a stale `skipped` beside a newer
|
|
813
|
+
* `failure` fails the verdict whatever order the pages arrive in.
|
|
709
814
|
*/
|
|
710
815
|
export function restVerificationFrom(
|
|
711
816
|
prRaw: string,
|
|
@@ -732,7 +837,11 @@ export function restVerificationFrom(
|
|
|
732
837
|
if (checks.length === 0) {
|
|
733
838
|
return { status: "pending", reason: "GitHub has not reported any checks yet", headSha };
|
|
734
839
|
}
|
|
735
|
-
|
|
840
|
+
// Same-name runs decide by recency, not page order (#1066): the flattened
|
|
841
|
+
// planes carry every attempt of a name, and first-match folding would let
|
|
842
|
+
// the oldest attempt answer for it.
|
|
843
|
+
const resolved = resolveNewestPerName(checks);
|
|
844
|
+
const failed = resolved.filter((c) => c.verdict === "failed");
|
|
736
845
|
if (failed.length > 0) {
|
|
737
846
|
return {
|
|
738
847
|
status: "failed",
|
|
@@ -740,7 +849,7 @@ export function restVerificationFrom(
|
|
|
740
849
|
headSha,
|
|
741
850
|
};
|
|
742
851
|
}
|
|
743
|
-
const pending =
|
|
852
|
+
const pending = resolved.filter((c) => c.verdict === "pending");
|
|
744
853
|
if (pending.length > 0) {
|
|
745
854
|
return {
|
|
746
855
|
status: "pending",
|
|
@@ -748,7 +857,7 @@ export function restVerificationFrom(
|
|
|
748
857
|
headSha,
|
|
749
858
|
};
|
|
750
859
|
}
|
|
751
|
-
return { status: "green", reason: `${
|
|
860
|
+
return { status: "green", reason: `${resolved.length} checks succeeded or were skipped`, headSha };
|
|
752
861
|
}
|
|
753
862
|
|
|
754
863
|
/**
|
|
@@ -2074,6 +2183,19 @@ export function makeTracker(
|
|
|
2074
2183
|
const rest = await verifyPrRest(runGh, cache, url, expectedHead, opts, hooks.onNotModified);
|
|
2075
2184
|
return rest ?? verification;
|
|
2076
2185
|
}
|
|
2186
|
+
if (verification.status === "green") {
|
|
2187
|
+
// A rollup context is one NAME, not one RUN (#1066): veltro#717
|
|
2188
|
+
// read eighteen green contexts while the same SHA carried a newer
|
|
2189
|
+
// `failure` run of a name whose rollup row still said `skipped`.
|
|
2190
|
+
// Recency ordering helps only when both rows arrive together; a
|
|
2191
|
+
// rollup that omits the newer run entirely cannot be ordered back
|
|
2192
|
+
// to correctness, so green is re-proven from the flattened REST
|
|
2193
|
+
// planes — the same evidence the empty-rollup path above trusts.
|
|
2194
|
+
// REST that cannot answer leaves this verdict standing, exactly as
|
|
2195
|
+
// a degraded plane leaves the #999 pending standing.
|
|
2196
|
+
const rest = await verifyPrRest(runGh, cache, url, expectedHead, opts, hooks.onNotModified);
|
|
2197
|
+
return rest ?? verification;
|
|
2198
|
+
}
|
|
2077
2199
|
if (verification.status !== "failed") return verification;
|
|
2078
2200
|
|
|
2079
2201
|
const detailsUrl = failedCheck(raw)?.detailsUrl;
|
package/src/types.ts
CHANGED
|
@@ -90,6 +90,12 @@ export interface Caps {
|
|
|
90
90
|
/** Wall-clock ceiling for one worker (90 min): a session that is merely
|
|
91
91
|
* stuck spends no turns, so turns alone cannot detect it. */
|
|
92
92
|
workerWallClockMs: number;
|
|
93
|
+
/** How long a live worker's transcript may stay unwritten before the
|
|
94
|
+
* daemon settles it as a progress stall (#1086). `null` derives it from the
|
|
95
|
+
* wall-clock ceiling — one third, so a hung run is caught long before its
|
|
96
|
+
* clock runs out. A single turn can legitimately run for minutes on a slow
|
|
97
|
+
* provider, so this reads *transcript writes*, never the turn counter. */
|
|
98
|
+
workerStallSilenceMs: number | null;
|
|
93
99
|
/** Failed implementation/CI attempts allowed before escalation. Operational
|
|
94
100
|
* continuations do not consume this budget. */
|
|
95
101
|
maxAttemptsPerIssue: number;
|
|
@@ -1831,6 +1837,13 @@ export const FAILURE_CLASSES = [
|
|
|
1831
1837
|
* 2026-08-23 failures were this, at 100/180 and 54/180 turns, while a third
|
|
1832
1838
|
* run on the same model finished green in the same window. */
|
|
1833
1839
|
"model-empty-stop",
|
|
1840
|
+
/** A live session stopped writing its transcript for longer than the
|
|
1841
|
+
* configured silence window, and the daemon settled it (#1086). Not a size
|
|
1842
|
+
* verdict: the work did not run out of budget, the session simply never
|
|
1843
|
+
* came back — the evidence names how long it was silent and at which turn.
|
|
1844
|
+
* When the attempt had already pushed a green PR, the settle path lands the
|
|
1845
|
+
* row `pushed-green` instead, so landed work is never charged anything. */
|
|
1846
|
+
"progress-stall",
|
|
1834
1847
|
"unknown",
|
|
1835
1848
|
] as const;
|
|
1836
1849
|
|
|
@@ -2131,6 +2144,14 @@ export interface RunRecord {
|
|
|
2131
2144
|
autoCompactionCount?: number;
|
|
2132
2145
|
/** omp session transcript, so a human can read what the worker actually did. */
|
|
2133
2146
|
sessionFile?: string;
|
|
2147
|
+
/**
|
|
2148
|
+
* When the run's transcript was last observed to grow, as epoch ms (#1086).
|
|
2149
|
+
* Recorded by the daemon's progress watch from the file's own mtime — the
|
|
2150
|
+
* write instant, not the observation instant — and read by `status` to show
|
|
2151
|
+
* a silence interval while the row is live. Absent means the pass has not
|
|
2152
|
+
* looked at this run (or it has no transcript yet), never "no progress".
|
|
2153
|
+
*/
|
|
2154
|
+
lastProgressAt?: number;
|
|
2134
2155
|
/**
|
|
2135
2156
|
* The exact `session-host` pid this run's Herdr representation was reported
|
|
2136
2157
|
* for, and the pane Herdr knows it as (#842).
|
|
@@ -3372,6 +3393,11 @@ export interface Store {
|
|
|
3372
3393
|
* predicate pick the window. Read-only.
|
|
3373
3394
|
*/
|
|
3374
3395
|
recentSpendSamples(project: string, limit: number): { turns: number; spendUsd: number }[];
|
|
3396
|
+
/** The newest runs carrying a usable per-turn latency (#1063): terminal
|
|
3397
|
+
* rows with turns and elapsed time recorded, newest first. Bounded by
|
|
3398
|
+
* `limit` rows like `recentSpendSamples` — the caller over-samples and lets
|
|
3399
|
+
* its own model-attribution filter pick the window. Read-only. */
|
|
3400
|
+
recentLatencySamples(project: string, limit: number): RunRecord[];
|
|
3375
3401
|
/** Total tracked `gh` calls between two UTC day keys, inclusive (#198). */
|
|
3376
3402
|
ghCallsBetween(sinceDay: string, untilDay: string): number;
|
|
3377
3403
|
/** Idempotence guard so a retry loop cannot page a human repeatedly for the
|
|
@@ -4034,6 +4060,11 @@ export const DEFAULT_CAPS: Caps = {
|
|
|
4034
4060
|
workerMaxTurns: 120,
|
|
4035
4061
|
workerMaxTurnsCeiling: 240,
|
|
4036
4062
|
workerWallClockMs: 90 * 60 * 1000,
|
|
4063
|
+
// Derived as a third of the wall-clock ceiling unless an operator names the
|
|
4064
|
+
// window: a hung run is caught with most of its budget unspent, while a
|
|
4065
|
+
// slow-but-writing session (measured fleet latency has reached
|
|
4066
|
+
// 1.5 min/turn) never trips it (#1086).
|
|
4067
|
+
workerStallSilenceMs: null,
|
|
4037
4068
|
maxAttemptsPerIssue: 2,
|
|
4038
4069
|
maxContinuationsPerIssue: 2,
|
|
4039
4070
|
};
|
package/src/verbs/server.ts
CHANGED
|
@@ -426,7 +426,8 @@ export function releaseRequirementRefusal(
|
|
|
426
426
|
const drain =
|
|
427
427
|
". Settle the fleet with a bounded drain: " +
|
|
428
428
|
`omp-conductor drain start${facts.project === undefined ? "" : ` --project ${facts.project}`} --until <deadline>` +
|
|
429
|
-
" — claiming pauses
|
|
429
|
+
" — claiming pauses until the deadline even once the fleet goes quiet (an emptied fleet opens" +
|
|
430
|
+
" the window rather than ending the drain early), and admission resumes at the deadline on its own.";
|
|
430
431
|
return (
|
|
431
432
|
`policy.release.requires includes ${requirement} and ${facts.unsettledRuns} run(s) have not settled` +
|
|
432
433
|
(parts.length === 0 ? "" : ` — ${parts.join("; ")}`) +
|
|
@@ -2292,22 +2293,25 @@ async function prUpdateVerb(
|
|
|
2292
2293
|
*/
|
|
2293
2294
|
|
|
2294
2295
|
/**
|
|
2295
|
-
* The run states a review revision may start from (#795).
|
|
2296
|
+
* The run states a review revision may start from (#795, #1101).
|
|
2296
2297
|
*
|
|
2297
2298
|
* A revision round resumes the exact run whose row owns the PR, so the
|
|
2298
|
-
* revisable states are exactly the terminal runs that
|
|
2299
|
-
* `pushed-green` row,
|
|
2300
|
-
* failed *after* pushing a green PR
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
2303
|
-
*
|
|
2299
|
+
* revisable states are exactly the terminal runs that own the named PR: a
|
|
2300
|
+
* settled `pushed-green` row, a `failed` / `killed` row — a run that capped or
|
|
2301
|
+
* failed *after* pushing a green PR — and, since #1101, a `stopped` row that
|
|
2302
|
+
* had already pushed when the operator stopped it. Ownership is proved by the
|
|
2303
|
+
* selection itself (`runsForProjectPr` matches rows that recorded the PR), so
|
|
2304
|
+
* a stopped run that never pushed can never reach this gate. Without the
|
|
2305
|
+
* stopped leg a stopped-with-green-PR run was both unreviewable (this verb)
|
|
2306
|
+
* and unreplaceable (the open-PR dispatch guard) at once — closing the PR by
|
|
2307
|
+
* hand was the only exit, which is exactly what #1096 hit.
|
|
2304
2308
|
*
|
|
2305
2309
|
* Closed on purpose: a live row (`running` / `claimed`) is already doing its
|
|
2306
2310
|
* own work, a `pushed-pending` PR is not green yet, and a `blocked` /
|
|
2307
|
-
* `orphaned` / `
|
|
2308
|
-
*
|
|
2309
|
-
* the
|
|
2310
|
-
*
|
|
2311
|
+
* `orphaned` / `merged` row is not work returned for revision. The set itself
|
|
2312
|
+
* is the shared `REVISABLE_RUN_STATES` in `decisions.ts` — the one definition
|
|
2313
|
+
* the review verb and the `pr-review-ready` watch gate on, so the two can
|
|
2314
|
+
* never drift (#844).
|
|
2311
2315
|
*/
|
|
2312
2316
|
|
|
2313
2317
|
async function prReviewVerb(
|