omp-conductor 0.15.13 → 0.16.1
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/REFERENCE.md +72 -2
- package/package.json +2 -1
- package/schema/config.schema.json +7 -0
- package/src/admission.ts +849 -0
- package/src/ask.ts +47 -0
- package/src/backups.ts +19 -7
- package/src/board.ts +1 -2
- package/src/briefs/orchestrator.md +62 -4
- package/src/cli.ts +26 -0
- package/src/commands/context.ts +3 -0
- package/src/commands/decision.ts +10 -1
- package/src/commands/doctor.ts +2 -0
- package/src/commands/message.ts +8 -1
- package/src/commands/restart.ts +15 -3
- package/src/commands/restore-db.ts +146 -0
- package/src/commands/stop.ts +24 -15
- package/src/commands/tail.ts +204 -44
- package/src/commands/unfreeze.ts +56 -0
- package/src/commands/watch.ts +77 -0
- package/src/config-schema.ts +13 -0
- package/src/config.ts +54 -0
- package/src/daemon.ts +255 -530
- package/src/dashboard/server.ts +2 -1
- package/src/decisions.ts +32 -7
- package/src/depends-on.ts +122 -0
- package/src/doctor.ts +297 -5
- package/src/escalate.ts +191 -19
- package/src/failure-class.ts +47 -0
- package/src/fleet.ts +168 -452
- package/src/gitops.ts +86 -1
- package/src/log.ts +40 -0
- package/src/model-fallback.ts +3 -2
- package/src/omp-settings.ts +114 -0
- package/src/omp.ts +39 -0
- package/src/orchestrator-tick.ts +7 -1
- package/src/reports.ts +124 -12
- package/src/session-host.ts +6 -0
- package/src/setup-wizard.ts +36 -0
- package/src/setup.ts +58 -1
- package/src/status-render.ts +445 -0
- package/src/stop-provenance.ts +53 -0
- package/src/store.ts +352 -11
- package/src/transcript.ts +1 -1
- package/src/types.ts +187 -4
- package/src/unblock.ts +1 -1
- package/src/upgrade-verify.ts +1 -1
- package/src/upgrade.ts +1 -2
- package/src/verbs/server.ts +25 -0
- package/src/worker.ts +358 -10
- package/src/worktree.ts +13 -1
package/src/types.ts
CHANGED
|
@@ -657,6 +657,26 @@ export interface ProjectConfig {
|
|
|
657
657
|
* when absent or unusable.
|
|
658
658
|
*/
|
|
659
659
|
modelFallbackThreshold?: number;
|
|
660
|
+
/**
|
|
661
|
+
* An opaque omp settings map layered into every worker session this project
|
|
662
|
+
* dispatches (#537). At dispatch it is materialised verbatim to a fleet-owned
|
|
663
|
+
* YAML overlay under the run's session directory — never inside the worktree,
|
|
664
|
+
* whose diff is a PR — and passed to the session as an omp settings overlay
|
|
665
|
+
* (`Settings.init({ configFiles: [<path>] })`), so it layers on top of both
|
|
666
|
+
* the daemon account's global config and any project `<cwd>/.omp/config.yml`
|
|
667
|
+
* without erasing either.
|
|
668
|
+
*
|
|
669
|
+
* Conductor validates YAML shape only: a non-mapping is dropped at load, and
|
|
670
|
+
* everything inside the map is omp's schema to own — an unknown key is omp's
|
|
671
|
+
* to reject, never conductor's to understand. A config edit takes effect on
|
|
672
|
+
* the next dispatch (the overlay is rewritten on every attempt), and a
|
|
673
|
+
* project without the field dispatches byte-for-byte as it always has.
|
|
674
|
+
*
|
|
675
|
+
* The retry keys derived from {@link modelFallbacks} are merged into the
|
|
676
|
+
* effective overlay (unless the map already names a `retry` mapping), which
|
|
677
|
+
* is where #539's staging lives.
|
|
678
|
+
*/
|
|
679
|
+
ompSettings?: Record<string, unknown>;
|
|
660
680
|
/**
|
|
661
681
|
* How a stuck run reaches a human, what to do when it cannot, and who runs
|
|
662
682
|
* the session that triages it. See {@link ORCHESTRATOR_MODES}.
|
|
@@ -751,6 +771,10 @@ export interface ConductorConfig {
|
|
|
751
771
|
version: typeof CONFIG_VERSION;
|
|
752
772
|
defaults: Caps;
|
|
753
773
|
projects: ProjectConfig[];
|
|
774
|
+
/** Absolute directory for restorable `conductor.db` snapshots; defaults to
|
|
775
|
+
* `<stateDir()>/backups/db` when omitted. A snapshot that cannot land there
|
|
776
|
+
* is `doctor`'s `db-backup` failure. */
|
|
777
|
+
dbBackupDir?: string;
|
|
754
778
|
}
|
|
755
779
|
|
|
756
780
|
/**
|
|
@@ -1102,6 +1126,11 @@ export const FAILURE_CLASSES = [
|
|
|
1102
1126
|
"settlement-stuck",
|
|
1103
1127
|
"provider-credit",
|
|
1104
1128
|
"provider-transient",
|
|
1129
|
+
/** A run drowned in in-session provider rate limits (repeated HTTP 429s) and
|
|
1130
|
+
* never got clear of the throttle before the cap swallowed it. Distinct from
|
|
1131
|
+
* `provider-credit` (402) and `provider-transient` (a single stream fault)
|
|
1132
|
+
* because its remedy is different (#573). */
|
|
1133
|
+
"provider-capacity",
|
|
1105
1134
|
/** A reviewer closed pushed-green or pushed-pending work without merging it:
|
|
1106
1135
|
* a review decision, not a worker failure. */
|
|
1107
1136
|
"returned-for-revision",
|
|
@@ -1151,6 +1180,30 @@ export interface BaseHealth {
|
|
|
1151
1180
|
checkedAt: number;
|
|
1152
1181
|
}
|
|
1153
1182
|
|
|
1183
|
+
/**
|
|
1184
|
+
* A per-repo merge freeze, set when a watched merge (or live base observation)
|
|
1185
|
+
* turns the base red and lifted mechanically on green or by the operator's
|
|
1186
|
+
* `unfreeze` verb. While `clearedAt` is absent the repo is frozen: `prMergeVerb`
|
|
1187
|
+
* refuses further merges to it with the `base-red-freeze` refusal naming the
|
|
1188
|
+
* suspected culprit. It is repo-scoped, never global, so sibling repos keep
|
|
1189
|
+
* merging while one base is broken.
|
|
1190
|
+
*/
|
|
1191
|
+
export interface BaseFreeze {
|
|
1192
|
+
repo: string;
|
|
1193
|
+
/** The merged SHA suspected of breaking the base. */
|
|
1194
|
+
culpritSha: string;
|
|
1195
|
+
/** Failing workflow/run evidence naming the break. */
|
|
1196
|
+
detail?: string;
|
|
1197
|
+
/** When the freeze was set (or last refreshed). */
|
|
1198
|
+
setAt: number;
|
|
1199
|
+
/** When the freeze was lifted — absent means the repo is actively frozen. */
|
|
1200
|
+
clearedAt?: number;
|
|
1201
|
+
/** Who lifted it: `daemon` for mechanical recovery, else the operator. */
|
|
1202
|
+
clearedBy?: string;
|
|
1203
|
+
/** Why it was lifted: `base-green` or the operator's override reason. */
|
|
1204
|
+
clearedReason?: string;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1154
1207
|
/**
|
|
1155
1208
|
* Execution state is separate from the tracker's own labels on purpose: labels
|
|
1156
1209
|
* are coarse and human-editable, while the loop needs to distinguish "pushed
|
|
@@ -1194,6 +1247,39 @@ export interface RunRecord {
|
|
|
1194
1247
|
/** Effective turn ceiling for this run; operators may only raise it. */
|
|
1195
1248
|
maxTurns: number;
|
|
1196
1249
|
spendUsd: number;
|
|
1250
|
+
/** How many in-session HTTP 429 responses the harness retried before the run
|
|
1251
|
+
* ended, counted by the worker from the session's `message_end` events while
|
|
1252
|
+
* the run was live (#573). Absent means the column predates the count or no
|
|
1253
|
+
* worker reported one — never "no rate limit happened": a healthy run
|
|
1254
|
+
* records 0. The classifier only reads it as "sustained" above a threshold,
|
|
1255
|
+
* so a single retried 429 is noise. */
|
|
1256
|
+
provider429Count?: number;
|
|
1257
|
+
/**
|
|
1258
|
+
* The model that actually wrote this run's messages, read from the newest
|
|
1259
|
+
* assistant `message_end`'s `AssistantMessage.model` (#539, #535 slice 1).
|
|
1260
|
+
* Distinct from {@link RunRecord.model} (what the daemon dispatched on):
|
|
1261
|
+
* this is what the harness resolved and actually used, present even for a run
|
|
1262
|
+
* that never failed over, so "which model wrote this" is answerable without
|
|
1263
|
+
* reading a transcript. Absent means the run predates the column or recorded
|
|
1264
|
+
* no message carrying a model.
|
|
1265
|
+
*/
|
|
1266
|
+
resolvedModel?: string;
|
|
1267
|
+
/** The provider that wrote them, from the same message. */
|
|
1268
|
+
resolvedProvider?: string;
|
|
1269
|
+
/**
|
|
1270
|
+
* Every within-run model fallback the harness applied (`retry_fallback_applied`),
|
|
1271
|
+
* newest first. The `to` target is what a settlement report names when a run
|
|
1272
|
+
* swapped providers mid-run instead of dying on a throttled primary (#539).
|
|
1273
|
+
*/
|
|
1274
|
+
retryFallbacks?: { from: string; to: string }[];
|
|
1275
|
+
/** `retry_fallback_succeeded` events: within-run fallbacks the harness recovered on. */
|
|
1276
|
+
retryFallbackSucceeded?: number;
|
|
1277
|
+
/** Assistant messages whose `retryRecovery.recovery === "model"` (#539). */
|
|
1278
|
+
modelRecoveries?: number;
|
|
1279
|
+
/** `auto_retry_start` events: in-session provider retries the harness ran. */
|
|
1280
|
+
autoRetryCount?: number;
|
|
1281
|
+
/** `auto_compaction_start` events: in-session context compactions. */
|
|
1282
|
+
autoCompactionCount?: number;
|
|
1197
1283
|
/** omp session transcript, so a human can read what the worker actually did. */
|
|
1198
1284
|
sessionFile?: string;
|
|
1199
1285
|
prUrl?: string;
|
|
@@ -1288,6 +1374,8 @@ export type AdmissionHoldReason =
|
|
|
1288
1374
|
| "plan-usage-cap"
|
|
1289
1375
|
| "shutting-down"
|
|
1290
1376
|
| "stale-base"
|
|
1377
|
+
| "file-lane"
|
|
1378
|
+
| "depends-on"
|
|
1291
1379
|
| "unroutable:no-repo-label"
|
|
1292
1380
|
| "unroutable:multiple-repo-labels"
|
|
1293
1381
|
| "unroutable:unknown-repo";
|
|
@@ -1298,6 +1386,14 @@ export interface AdmissionHoldSummary {
|
|
|
1298
1386
|
count: number;
|
|
1299
1387
|
/** Queue-order sample, capped before persistence and rendering. */
|
|
1300
1388
|
issues: number[];
|
|
1389
|
+
/**
|
|
1390
|
+
* Per-issue detail, aligned by index with {@link AdmissionHoldSummary.issues}
|
|
1391
|
+
* and capped the same way. The file-lane interlock (#555) fills it so an
|
|
1392
|
+
* operator reading status or the digest can see *which file* blocked the
|
|
1393
|
+
* held issue and *which running issue* holds it — the same question the
|
|
1394
|
+
* plain `reason` group answers only with a count.
|
|
1395
|
+
*/
|
|
1396
|
+
details?: string[];
|
|
1301
1397
|
}
|
|
1302
1398
|
|
|
1303
1399
|
/** Persisted outcome of the latest completed dispatch tick. */
|
|
@@ -1437,9 +1533,29 @@ export interface ReportRecord {
|
|
|
1437
1533
|
updatedAt: number;
|
|
1438
1534
|
/** Backoff gate: a `pending` row is due only once this has passed. */
|
|
1439
1535
|
nextAttemptAt: number;
|
|
1440
|
-
|
|
1441
|
-
* body rather than inferred from the HTTP status.
|
|
1536
|
+
/** Telegram's own id for the delivered message, read out of the response
|
|
1537
|
+
* body rather than inferred from the HTTP status. A report split into
|
|
1538
|
+
* several messages records the first part's id here; {@link messageIds}
|
|
1539
|
+
* carries every part. */
|
|
1442
1540
|
messageId?: number;
|
|
1541
|
+
/** Every Telegram message id a split delivery accepted, in order. Absent
|
|
1542
|
+
* for rows delivered before multi-part sends existed (#566), and whenever
|
|
1543
|
+
* Telegram answered without an id — `ok: true` is the verdict, the id is a
|
|
1544
|
+
* courtesy. */
|
|
1545
|
+
messageIds?: number[];
|
|
1546
|
+
/**
|
|
1547
|
+
* Parts of the current delivery contract already accepted, persisted as the
|
|
1548
|
+
* retry's resume point. The split is a pure function of the message text, so
|
|
1549
|
+
* a count is the whole state: a retry whose message is byte-identical to the
|
|
1550
|
+
* attempt that wrote this watermark (see {@link sentPartsHash}) starts at
|
|
1551
|
+
* part `sentParts` instead of re-sending confirmed parts (#566).
|
|
1552
|
+
*/
|
|
1553
|
+
sentParts?: number;
|
|
1554
|
+
/** SHA-256 of the message text the `sentParts` watermark was written under.
|
|
1555
|
+
* Progress is only valid while the text is unchanged — a report flagged
|
|
1556
|
+
* ambiguous re-renders with a possible-repeat banner, which reshapes the
|
|
1557
|
+
* split, so the watermark must not carry across it. */
|
|
1558
|
+
sentPartsHash?: string;
|
|
1443
1559
|
deliveredAt?: number;
|
|
1444
1560
|
/** Bounded text of the last known failure, surfaced verbatim by `status`. */
|
|
1445
1561
|
lastError?: string;
|
|
@@ -1555,10 +1671,22 @@ export type DecisionState = (typeof DECISION_STATES)[number];
|
|
|
1555
1671
|
*/
|
|
1556
1672
|
export const DECISION_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
1557
1673
|
|
|
1674
|
+
/** What a pending decision row is owed — answered by a human, or a condition
|
|
1675
|
+
* the orchestrator set for itself with no human in the loop. The split is a
|
|
1676
|
+
* stored column, never inferred from the wording or from whether a condition
|
|
1677
|
+
* is attached: a real question may legitimately carry a condition ("ask me
|
|
1678
|
+
* once this PR merges"), and a watch is defined by having no human, not by
|
|
1679
|
+
* having one. */
|
|
1680
|
+
export type DecisionKind = "question" | "watch";
|
|
1681
|
+
|
|
1558
1682
|
/** One question put to the operator, and its answer if it has one. */
|
|
1559
1683
|
export interface DecisionRecord {
|
|
1560
1684
|
id: string;
|
|
1561
1685
|
project: string;
|
|
1686
|
+
/** Whether this row is a question a human must answer or a watch the
|
|
1687
|
+
* orchestrator set for itself. A watch turns up under its own heading and
|
|
1688
|
+
* is never offered to the operator to resolve. */
|
|
1689
|
+
kind: DecisionKind;
|
|
1562
1690
|
/** The question verbatim, as it was sent. Re-asking must not reword it. */
|
|
1563
1691
|
question: string;
|
|
1564
1692
|
/** What is waiting on the answer — an issue, a release, a PR. Free text,
|
|
@@ -1583,6 +1711,11 @@ export interface DecisionRecord {
|
|
|
1583
1711
|
export interface DecisionDraft {
|
|
1584
1712
|
project: string;
|
|
1585
1713
|
question: string;
|
|
1714
|
+
/** `"watch"` marks a row the orchestrator set for itself. Absent means a
|
|
1715
|
+
* question a human must answer — the default, so every existing caller
|
|
1716
|
+
* (`message` with a non-material category, `decision open`) stays a
|
|
1717
|
+
* question without remembering to say so. */
|
|
1718
|
+
kind?: DecisionKind;
|
|
1586
1719
|
blocks?: string;
|
|
1587
1720
|
condition?: string;
|
|
1588
1721
|
at: number;
|
|
@@ -1642,6 +1775,35 @@ export interface Store {
|
|
|
1642
1775
|
project: string,
|
|
1643
1776
|
sinceEpochMs: number,
|
|
1644
1777
|
): { repo: string; baseRef?: string }[];
|
|
1778
|
+
/** The per-repo base-red freeze row, active or cleared, for one routed repo. */
|
|
1779
|
+
baseFreeze(project: string, repo: string): BaseFreeze | undefined;
|
|
1780
|
+
/** Every base-red freeze row for a project, active first, for `status`. */
|
|
1781
|
+
freezes(project: string): BaseFreeze[];
|
|
1782
|
+
/**
|
|
1783
|
+
* Set or refresh an *active* base-red freeze for one repo, naming the
|
|
1784
|
+
* suspected culprit merge and the failing evidence. Refreshing an
|
|
1785
|
+
* already-active freeze keeps the repo frozen while the same head is still
|
|
1786
|
+
* red. Returns true only when it *newly* froze the repo — a transition worth
|
|
1787
|
+
* surfacing as a material event — and false for a pure refresh so the queue
|
|
1788
|
+
* digest is not flooded by per-tick re-observation of a still-red base.
|
|
1789
|
+
*/
|
|
1790
|
+
setBaseFreeze(
|
|
1791
|
+
project: string,
|
|
1792
|
+
freeze: { repo: string; culpritSha: string; detail?: string; setAt?: number },
|
|
1793
|
+
): boolean;
|
|
1794
|
+
/**
|
|
1795
|
+
* Lift an active freeze — the mechanical green recovery or the operator's
|
|
1796
|
+
* `unfreeze`. Records who lifted it and the ledger reason. Returns true only
|
|
1797
|
+
* when an active freeze was actually lifted (so a recovery/override event is
|
|
1798
|
+
* emitted once, never on an already-clear row).
|
|
1799
|
+
*/
|
|
1800
|
+
clearBaseFreeze(
|
|
1801
|
+
project: string,
|
|
1802
|
+
repo: string,
|
|
1803
|
+
by: string,
|
|
1804
|
+
reason: string,
|
|
1805
|
+
at?: number,
|
|
1806
|
+
): boolean;
|
|
1645
1807
|
/** Newest attempt per issue that preserved work or failed to, so `status`
|
|
1646
1808
|
* can name every WIP tip a re-claim would build on and every tree that is
|
|
1647
1809
|
* still the only copy. */
|
|
@@ -1808,8 +1970,21 @@ export interface Store {
|
|
|
1808
1970
|
* means somebody else claimed it first — the guard against the same report
|
|
1809
1971
|
* being concurrently in flight twice. */
|
|
1810
1972
|
claimReport(id: string, attemptId: string, at: number): ReportRecord | undefined;
|
|
1811
|
-
/** Record the message
|
|
1812
|
-
|
|
1973
|
+
/** Record the message ids Telegram returned for every accepted part, and
|
|
1974
|
+
* the moment the last one landed. `messageId` (the first part's id) is
|
|
1975
|
+
* derived here, so the two can never disagree. */
|
|
1976
|
+
markReportDelivered(id: string, attemptId: string, messageIds: readonly number[], at: number): boolean;
|
|
1977
|
+
/** Record progress mid-split: the first `sentParts` parts of the delivery
|
|
1978
|
+
* contract fingerprinted by `sentPartsHash` are confirmed accepted. Written
|
|
1979
|
+
* before the next part ships, so a crash in the window can only ever
|
|
1980
|
+
* re-send the part that was in flight, never confirmed ones. */
|
|
1981
|
+
markReportPartsSent(
|
|
1982
|
+
id: string,
|
|
1983
|
+
attemptId: string,
|
|
1984
|
+
sentParts: number,
|
|
1985
|
+
sentPartsHash: string,
|
|
1986
|
+
at: number,
|
|
1987
|
+
): boolean;
|
|
1813
1988
|
/** The attempt ended without an answer: Telegram may hold the message. The
|
|
1814
1989
|
* row *stays* `sending`, because that state means exactly "outcome unknown",
|
|
1815
1990
|
* and is flagged {@link ReportRecord.ambiguous} so the retry that stale
|
|
@@ -2182,6 +2357,14 @@ export const VERB_REFUSALS = [
|
|
|
2182
2357
|
"checks-not-green",
|
|
2183
2358
|
/** Another merge is in flight for this project. */
|
|
2184
2359
|
"merge-in-flight",
|
|
2360
|
+
/**
|
|
2361
|
+
* The routed repository's base branch is frozen because a watched merge (or
|
|
2362
|
+
* live base observation) turned it red — a base-red-freeze. Further merges to
|
|
2363
|
+
* that repo stay refused until the base is observed green again or the
|
|
2364
|
+
* operator lifts the freeze with `omp-conductor unfreeze <repo>`; sibling
|
|
2365
|
+
* repos are unaffected.
|
|
2366
|
+
*/
|
|
2367
|
+
"base-red-freeze",
|
|
2185
2368
|
/** The label is not in this project's own vocabulary. */
|
|
2186
2369
|
"label-not-in-vocabulary",
|
|
2187
2370
|
/** The label is a lifecycle label; those transitions stay the daemon's (#26). */
|
package/src/unblock.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* failed-attempt budget.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import { hasContinuationBudget, hasFailedAttemptBudget } from "./
|
|
38
|
+
import { hasContinuationBudget, hasFailedAttemptBudget } from "./admission.ts";
|
|
39
39
|
import { projectLabels } from "./label-projection.ts";
|
|
40
40
|
import { LIVE_STATES } from "./store.ts";
|
|
41
41
|
import type { Caps, ProjectConfig, RunRecord, Store, Tracker } from "./types.ts";
|
package/src/upgrade-verify.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { DEFAULT_PORT } from "./lifecycle.ts";
|
|
|
26
26
|
import { dbPath, openStore } from "./store.ts";
|
|
27
27
|
import { appendJournal, readUpgradeJournal, upgradeJournalPath, type UpgradeCheck, type UpgradeJournalEntry } from "./upgrade-journal.ts";
|
|
28
28
|
import type { DoctorReport } from "./doctor.ts";
|
|
29
|
-
import type { FleetLayers } from "./
|
|
29
|
+
import type { FleetLayers } from "./status-render.ts";
|
|
30
30
|
import type { ReportDraft, ReportKind } from "./types.ts";
|
|
31
31
|
|
|
32
32
|
/** One command run to completion with captured output. The transient unit, the
|
package/src/upgrade.ts
CHANGED
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
LEGACY_HERDR_SESSION_HINT,
|
|
9
9
|
resolveHerdrSession,
|
|
10
10
|
telegramStateDir,
|
|
11
|
-
type DispatchLayer,
|
|
12
|
-
type FleetLayers,
|
|
13
11
|
} from "./fleet.ts";
|
|
12
|
+
import type { DispatchLayer, FleetLayers } from "./status-render.ts";
|
|
14
13
|
import { livingDaemon, restartDaemon } from "./lifecycle.ts";
|
|
15
14
|
import { configBackupDir, configPath, findProject, loadConfig, resolveCaps, stateDir, writeConfigRaw } from "./config.ts";
|
|
16
15
|
import { renderBriefForProject } from "./setup.ts";
|
package/src/verbs/server.ts
CHANGED
|
@@ -884,6 +884,31 @@ async function prMergeVerb(
|
|
|
884
884
|
if (paused !== undefined) return paused;
|
|
885
885
|
}
|
|
886
886
|
|
|
887
|
+
// Base-red-freeze gate (#283): a repo whose base is frozen must not accept
|
|
888
|
+
// another merge, no matter how green this PR's own checks look. Deliberately
|
|
889
|
+
// placed here — after target-repo identity resolution, before the merge lock
|
|
890
|
+
// and every `gh` call — so the refusal is a verb-level assertion, not a
|
|
891
|
+
// daemon-side flag the merge path could miss. Only the frozen repo is gated:
|
|
892
|
+
// sibling repos keep merging. The freeze lifts mechanically on a green base
|
|
893
|
+
// re-observation or via the operator's sanctioned `omp-conductor unfreeze
|
|
894
|
+
// <repo>` — never by label surgery or a DB edit.
|
|
895
|
+
const freezeRepo = target === undefined ? routedRepo?.name : target.repo;
|
|
896
|
+
if (freezeRepo !== undefined) {
|
|
897
|
+
const freeze = deps.store.baseFreeze(project.name, freezeRepo);
|
|
898
|
+
if (freeze !== undefined && freeze.clearedAt === undefined) {
|
|
899
|
+
return refuse(
|
|
900
|
+
"base-red-freeze",
|
|
901
|
+
`refused: merges to ${freezeRepo} are frozen because its base is red at ` +
|
|
902
|
+
`${freeze.culpritSha.slice(0, 8)} (base-red-freeze). ${freeze.detail ?? ""} ` +
|
|
903
|
+
"This is not a merge-lock or check verdict: the base that merges land on is broken, so " +
|
|
904
|
+
"`conductor_pr_merge` for this repo is refused until the base is observed green again or the " +
|
|
905
|
+
`operator lifts the freeze with \`omp-conductor unfreeze ${freezeRepo}\`. The override is the sanctioned ` +
|
|
906
|
+
"path — never work around this refusal with label surgery or a DB edit.",
|
|
907
|
+
issue,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
887
912
|
// Taken before any network call, so two concurrent callers contend here
|
|
888
913
|
// rather than both spending a `gh` round trip and racing at the merge.
|
|
889
914
|
const holderId = randomUUID();
|