omp-conductor 0.15.6 → 0.15.8
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 +103 -27
- package/package.json +1 -1
- package/schema/config.schema.json +39 -0
- package/src/briefs/orchestrator.md +34 -10
- package/src/briefs/policy.md +8 -3
- package/src/cli.ts +62 -1
- package/src/config-schema.ts +20 -0
- package/src/config.ts +43 -0
- package/src/escalate.ts +69 -1
- package/src/fleet.ts +19 -39
- package/src/lifecycle.ts +164 -21
- package/src/orchestrator-tick.ts +96 -7
- package/src/reports.ts +49 -2
- package/src/setup-discover.ts +425 -0
- package/src/setup-wizard.ts +108 -34
- package/src/setup.ts +25 -1
- package/src/store.ts +5 -1
- package/src/types.ts +34 -0
- package/src/verbs/actions.ts +407 -4
- package/src/verbs/protocol.ts +2 -2
- package/src/verbs/server.ts +141 -27
package/src/setup-wizard.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
resolveCaps,
|
|
22
22
|
saveConfig,
|
|
23
23
|
} from "./config.ts";
|
|
24
|
+
import { claimedTelegramTopics } from "./escalate.ts";
|
|
24
25
|
import { hostRamBytes, recommendedMaxWorkers, workerOvercommit } from "./host.ts";
|
|
25
26
|
import {
|
|
26
27
|
prepareConductor,
|
|
@@ -42,6 +43,11 @@ import {
|
|
|
42
43
|
} from "./setup-host.ts";
|
|
43
44
|
import { runGraphInstall, runHostInstall } from "./setup-install.ts";
|
|
44
45
|
import { probeGates, probeProse, probeRepoMap, type ProbedGate, type ProbeTarget } from "./setup-probe.ts";
|
|
46
|
+
import {
|
|
47
|
+
discoverFacts,
|
|
48
|
+
type DiscoveredFacts,
|
|
49
|
+
type DiscoveryTarget,
|
|
50
|
+
} from "./setup-discover.ts";
|
|
45
51
|
import {
|
|
46
52
|
AMEND_AREAS,
|
|
47
53
|
BASE_FRESHNESS_CHOICES,
|
|
@@ -459,10 +465,12 @@ async function askAuthority(
|
|
|
459
465
|
* shape cannot be added without a question to ask about it — an unasked shape
|
|
460
466
|
* would silently take the deny default and read as a decision afterwards.
|
|
461
467
|
*
|
|
462
|
-
* No fleet vocabulary here
|
|
463
|
-
*
|
|
468
|
+
* No fleet-specific vocabulary here (#122): these are generic release acts.
|
|
469
|
+
* Version preparation becomes executable only when a repo declares its version
|
|
470
|
+
* file; package names, suite pins and deployment topology still stay in policy.
|
|
464
471
|
*/
|
|
465
472
|
const RELEASE_SHAPE_QUESTIONS: { readonly [K in (typeof RELEASE_SHAPES)[number]]: string } = {
|
|
473
|
+
"version-bump-pr": "open and land an exact version-only pull request before tagging",
|
|
466
474
|
"git-tag": "create git tags (`git tag v1.2.3`)",
|
|
467
475
|
"git-push-tags": "push tags to the remote (`git push --follow-tags`)",
|
|
468
476
|
"package-publish": "publish packages (`npm publish` and equivalents)",
|
|
@@ -489,14 +497,26 @@ async function askJudgment(
|
|
|
489
497
|
ui: WizardUi,
|
|
490
498
|
grants: ResolvedGrants,
|
|
491
499
|
prior: OperatorJudgment,
|
|
500
|
+
roadmapChoices: readonly string[] = [],
|
|
492
501
|
): Promise<OperatorJudgment> {
|
|
493
502
|
// Always asked. A tracker shows what is open, never what matters, and an
|
|
494
503
|
// orchestrator that cannot rank work grooms by recency — which is how a stale
|
|
495
504
|
// issue outranks the thing being shipped this month.
|
|
505
|
+
let roadmapSeed = prior.roadmap ?? "";
|
|
506
|
+
if (roadmapSeed === "" && roadmapChoices.length > 0) {
|
|
507
|
+
const other = "Type a different roadmap or priority";
|
|
508
|
+
const picked = await ui.select(
|
|
509
|
+
"Roadmap candidates found in GitHub",
|
|
510
|
+
[...roadmapChoices.map((label) => ({ label })), { label: other }],
|
|
511
|
+
{ initialIndex: 0 },
|
|
512
|
+
);
|
|
513
|
+
if (picked === undefined) throw new Cancelled();
|
|
514
|
+
roadmapSeed = picked === other ? "" : picked;
|
|
515
|
+
}
|
|
496
516
|
const roadmap = await ask(
|
|
497
517
|
ui,
|
|
498
518
|
"Where does the roadmap live, and what is the current priority?",
|
|
499
|
-
|
|
519
|
+
roadmapSeed,
|
|
500
520
|
);
|
|
501
521
|
const judgment: OperatorJudgment = { ...prior, ...(roadmap.length === 0 ? {} : { roadmap }) };
|
|
502
522
|
|
|
@@ -735,7 +755,12 @@ function priorProject(existing: ConductorConfig | undefined, name: string | unde
|
|
|
735
755
|
* prompts or the defaults they pre-fill from: the value shown is always the value
|
|
736
756
|
* that would otherwise be carried through.
|
|
737
757
|
*/
|
|
738
|
-
type AreaAsker = (
|
|
758
|
+
type AreaAsker = (
|
|
759
|
+
ui: WizardUi,
|
|
760
|
+
a: SetupAnswers,
|
|
761
|
+
probes: SetupProbes,
|
|
762
|
+
discovered?: DiscoveredFacts,
|
|
763
|
+
) => Promise<SetupAnswers>;
|
|
739
764
|
|
|
740
765
|
/**
|
|
741
766
|
* The repo-reading half of onboarding, injected rather than called directly.
|
|
@@ -747,6 +772,8 @@ type AreaAsker = (ui: WizardUi, a: SetupAnswers, probes: SetupProbes) => Promise
|
|
|
747
772
|
* peer, a private repo, or a model that answered in prose.
|
|
748
773
|
*/
|
|
749
774
|
export interface SetupProbes {
|
|
775
|
+
/** Finds exact checkout/GitHub facts. `{}` seeds nothing. */
|
|
776
|
+
discover(target: DiscoveryTarget): Promise<DiscoveredFacts>;
|
|
750
777
|
/** Proposes a repo's pre-push gates by reading its CI. `[]` seeds nothing. */
|
|
751
778
|
gates(ui: WizardUi, target: ProbeTarget): Promise<ProbedGate[]>;
|
|
752
779
|
/**
|
|
@@ -758,12 +785,77 @@ export interface SetupProbes {
|
|
|
758
785
|
|
|
759
786
|
/** Reads each repo to propose answers. */
|
|
760
787
|
export const DEFAULT_PROBES: SetupProbes = {
|
|
788
|
+
discover: (target) => discoverFacts(target),
|
|
761
789
|
gates: (ui, target) => probeGates(ui, target),
|
|
762
790
|
prose: (ui, a) => proseFromRepos(ui, a),
|
|
763
791
|
};
|
|
764
792
|
|
|
765
793
|
/** `--no-ai`, and every wizard test: nothing is read, nothing is proposed. */
|
|
766
|
-
export const NO_PROBES: SetupProbes = {
|
|
794
|
+
export const NO_PROBES: SetupProbes = {
|
|
795
|
+
discover: async () => ({}),
|
|
796
|
+
gates: async () => [],
|
|
797
|
+
prose: async () => ({}),
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
/** Exact facts replace only fresh defaults; saved operator answers always win. */
|
|
801
|
+
function seedFromDiscovery(seed: SetupAnswers, found: DiscoveredFacts): SetupAnswers {
|
|
802
|
+
const labels = new Map((found.labels ?? []).map((name) => [name.toLowerCase(), name]));
|
|
803
|
+
const trackerRepo = seed.trackerRepo || found.trackerRepo || "";
|
|
804
|
+
const targetRepos =
|
|
805
|
+
seed.targetRepos.length > 0 ||
|
|
806
|
+
found.routingKey === undefined ||
|
|
807
|
+
found.cloneUrl === undefined
|
|
808
|
+
? seed.targetRepos
|
|
809
|
+
: [
|
|
810
|
+
{
|
|
811
|
+
name: found.routingKey,
|
|
812
|
+
cloneUrl: found.cloneUrl,
|
|
813
|
+
defaultBranch: found.defaultBranch ?? SETUP_DEFAULTS.defaultBranch,
|
|
814
|
+
gates: [],
|
|
815
|
+
},
|
|
816
|
+
];
|
|
817
|
+
return {
|
|
818
|
+
...seed,
|
|
819
|
+
trackerRepo,
|
|
820
|
+
queueLabel: labels.get(seed.queueLabel.toLowerCase()) ?? seed.queueLabel,
|
|
821
|
+
stateLabels: {
|
|
822
|
+
inProgress: labels.get(seed.stateLabels.inProgress.toLowerCase()) ?? seed.stateLabels.inProgress,
|
|
823
|
+
blocked: labels.get(seed.stateLabels.blocked.toLowerCase()) ?? seed.stateLabels.blocked,
|
|
824
|
+
failed: labels.get(seed.stateLabels.failed.toLowerCase()) ?? seed.stateLabels.failed,
|
|
825
|
+
},
|
|
826
|
+
targetRepos,
|
|
827
|
+
policy: {
|
|
828
|
+
merge: {
|
|
829
|
+
...seed.policy.merge,
|
|
830
|
+
requiredChecks: found.requiredChecks ?? seed.policy.merge.requiredChecks,
|
|
831
|
+
},
|
|
832
|
+
release: {
|
|
833
|
+
...seed.policy.release,
|
|
834
|
+
artefacts: found.artefacts ?? seed.policy.release.artefacts,
|
|
835
|
+
environments: found.environments ?? seed.policy.release.environments,
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function showDiscovery(ui: WizardUi, found: DiscoveredFacts): void {
|
|
842
|
+
if ((found.evidence?.length ?? 0) > 0) {
|
|
843
|
+
ui.notify(
|
|
844
|
+
[
|
|
845
|
+
"Discovered setup defaults:",
|
|
846
|
+
...(found.evidence ?? []).map((line) => ` ${line}`),
|
|
847
|
+
"Every value remains editable at its prompt.",
|
|
848
|
+
].join("\n"),
|
|
849
|
+
"info",
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if ((found.skipped?.length ?? 0) > 0) {
|
|
853
|
+
ui.notify(
|
|
854
|
+
["Some setup discovery was skipped; today's typed defaults remain:", ...(found.skipped ?? []).map((line) => ` ${line}`)].join("\n"),
|
|
855
|
+
"warning",
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
767
859
|
|
|
768
860
|
/**
|
|
769
861
|
* The brief's two prose halves, drafted against **every** configured repo.
|
|
@@ -1047,14 +1139,14 @@ const askWorkerModel: AreaAsker = async (ui, a) => {
|
|
|
1047
1139
|
|
|
1048
1140
|
/** The two ownership questions, then the mechanical gate one shape at a time:
|
|
1049
1141
|
* together they are what decides what an unattended fleet may do unasked. */
|
|
1050
|
-
const askAuthorityArea: AreaAsker = async (ui, a) => {
|
|
1142
|
+
const askAuthorityArea: AreaAsker = async (ui, a, _probes, discovered) => {
|
|
1051
1143
|
const authority = await askAuthority(ui, a.authority);
|
|
1052
1144
|
const releaseGrants = await askReleaseGrants(ui, a.releaseGrants);
|
|
1053
1145
|
// Asked here because it is the same decision one layer down: the grants say
|
|
1054
1146
|
// who may act, and these say where that permission stops. Amending authority
|
|
1055
1147
|
// therefore re-asks the boundary, which is the point — a grant widened without
|
|
1056
1148
|
// restating the boundary is how a delegated release loses its end.
|
|
1057
|
-
const judgment = await askJudgment(ui, releaseGrants, a.judgment ?? {});
|
|
1149
|
+
const judgment = await askJudgment(ui, releaseGrants, a.judgment ?? {}, discovered?.roadmaps);
|
|
1058
1150
|
return { ...a, authority, releaseGrants, judgment };
|
|
1059
1151
|
};
|
|
1060
1152
|
|
|
@@ -1117,7 +1209,7 @@ async function askTelegramTopicId(
|
|
|
1117
1209
|
stateDir: string,
|
|
1118
1210
|
prior: number | undefined,
|
|
1119
1211
|
): Promise<number | undefined> {
|
|
1120
|
-
const claimed =
|
|
1212
|
+
const claimed = claimedTelegramTopics(stateDir);
|
|
1121
1213
|
const manual = "Enter thread id manually";
|
|
1122
1214
|
const none = "None — flat chat (0.13 behaviour)";
|
|
1123
1215
|
if (claimed.length > 0) {
|
|
@@ -1155,29 +1247,6 @@ async function askTelegramTopicId(
|
|
|
1155
1247
|
return n;
|
|
1156
1248
|
}
|
|
1157
1249
|
|
|
1158
|
-
function readClaimedTelegramTopics(stateDir: string): Array<{ threadId: number; name: string }> {
|
|
1159
|
-
try {
|
|
1160
|
-
const raw: unknown = JSON.parse(readFileSync(join(stateDir, "threads.json"), "utf8"));
|
|
1161
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return [];
|
|
1162
|
-
if (!("threads" in raw)) return [];
|
|
1163
|
-
const threads = raw.threads;
|
|
1164
|
-
if (typeof threads !== "object" || threads === null || Array.isArray(threads)) return [];
|
|
1165
|
-
const out: Array<{ threadId: number; name: string }> = [];
|
|
1166
|
-
for (const [id, entry] of Object.entries(threads)) {
|
|
1167
|
-
const threadId = Number(id);
|
|
1168
|
-
if (!Number.isFinite(threadId) || !Number.isSafeInteger(threadId)) continue;
|
|
1169
|
-
let name = id;
|
|
1170
|
-
if (typeof entry === "object" && entry !== null && "name" in entry) {
|
|
1171
|
-
const candidate = entry.name;
|
|
1172
|
-
if (typeof candidate === "string" && candidate.trim() !== "") name = candidate.trim();
|
|
1173
|
-
}
|
|
1174
|
-
out.push({ threadId, name });
|
|
1175
|
-
}
|
|
1176
|
-
return out;
|
|
1177
|
-
} catch {
|
|
1178
|
-
return [];
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
1250
|
|
|
1182
1251
|
/** How loud the orchestrator is and when the operator permits interruptions. */
|
|
1183
1252
|
const askReporting: AreaAsker = async (ui, a) => {
|
|
@@ -1432,12 +1501,17 @@ async function collectAnswers(
|
|
|
1432
1501
|
probes: SetupProbes,
|
|
1433
1502
|
opts: { added?: boolean } = {},
|
|
1434
1503
|
): Promise<SetupAnswers> {
|
|
1504
|
+
// Discovery is a fresh-interview default only. Re-runs start from the saved
|
|
1505
|
+
// project, so an operator-edited value is never re-guessed.
|
|
1506
|
+
const discovered = prior === undefined ? await probes.discover({ cwd: process.cwd() }) : {};
|
|
1507
|
+
showDiscovery(ui, discovered);
|
|
1435
1508
|
// Added projects seed under projects/<name>/; a first install and a re-interview
|
|
1436
1509
|
// of an existing project keep the flat (or already-on-disk) roots.
|
|
1437
|
-
const
|
|
1510
|
+
const baseSeed =
|
|
1438
1511
|
prior === undefined
|
|
1439
1512
|
? defaultAnswers(projectArg ?? "", { added: opts.added === true })
|
|
1440
1513
|
: answersFromProject(prior);
|
|
1514
|
+
const seed = prior === undefined ? seedFromDiscovery(baseSeed, discovered) : baseSeed;
|
|
1441
1515
|
|
|
1442
1516
|
const projectName = await askValid(
|
|
1443
1517
|
ui,
|
|
@@ -1462,8 +1536,8 @@ async function collectAnswers(
|
|
|
1462
1536
|
// routed repo, under one root.
|
|
1463
1537
|
a = await askGraph(ui, a, probes);
|
|
1464
1538
|
a = await askCaps(ui, a, probes);
|
|
1465
|
-
a = await askAuthorityArea(ui, a, probes);
|
|
1466
|
-
a = await askPolicy(ui, a, probes);
|
|
1539
|
+
a = await askAuthorityArea(ui, a, probes, discovered);
|
|
1540
|
+
a = await askPolicy(ui, a, probes, discovered);
|
|
1467
1541
|
a = await askWorkerModel(ui, a, probes);
|
|
1468
1542
|
a = await askEscalation(ui, a, probes);
|
|
1469
1543
|
a = await askReporting(ui, a, probes);
|
package/src/setup.ts
CHANGED
|
@@ -131,7 +131,14 @@ export interface SetupAnswers {
|
|
|
131
131
|
queueLabel: string;
|
|
132
132
|
stateLabels: { inProgress: string; blocked: string; failed: string };
|
|
133
133
|
routingLabelPrefix: string;
|
|
134
|
-
targetRepos: {
|
|
134
|
+
targetRepos: {
|
|
135
|
+
name: string;
|
|
136
|
+
cloneUrl: string;
|
|
137
|
+
defaultBranch: string;
|
|
138
|
+
gates: { cmd: string; cwd: string }[];
|
|
139
|
+
migrations?: { dir: string };
|
|
140
|
+
release?: { versionFile: string };
|
|
141
|
+
}[];
|
|
135
142
|
caps: Partial<Caps>;
|
|
136
143
|
/**
|
|
137
144
|
* Model pattern for worker sessions, in omp's model/role syntax. Absent means
|
|
@@ -170,6 +177,13 @@ export interface SetupAnswers {
|
|
|
170
177
|
* leave one to be defaulted by whichever reader gets there first.
|
|
171
178
|
*/
|
|
172
179
|
policy: ProjectPolicy;
|
|
180
|
+
/**
|
|
181
|
+
* Hand-edited recovery merge authorizations carried through setup unchanged.
|
|
182
|
+
* The wizard never grants one; forgetting them during an unrelated amend
|
|
183
|
+
* would strand an in-progress recovery, while inventing one would weaken the
|
|
184
|
+
* merge provenance gate.
|
|
185
|
+
*/
|
|
186
|
+
recoveryMerges?: ProjectConfig["recoveryMerges"];
|
|
173
187
|
/**
|
|
174
188
|
* Whether to render `ORCHESTRATOR.md` into the project's workspace root. Not
|
|
175
189
|
* part of the config — the brief is the operator's file, and the conductor
|
|
@@ -702,6 +716,8 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
702
716
|
...(graphRoot === undefined || graphRoot.length === 0
|
|
703
717
|
? {}
|
|
704
718
|
: { graphProject: graphProjectPath(graphRoot, r.name) }),
|
|
719
|
+
...(r.migrations === undefined ? {} : { migrations: { ...r.migrations } }),
|
|
720
|
+
...(r.release === undefined ? {} : { release: { ...r.release } }),
|
|
705
721
|
};
|
|
706
722
|
}
|
|
707
723
|
|
|
@@ -749,6 +765,9 @@ export function buildProject(a: SetupAnswers): ProjectConfig {
|
|
|
749
765
|
// Written out in full for the same reason: the file then says what a merge
|
|
750
766
|
// and a release require without anyone having to know a default (#129).
|
|
751
767
|
policy: clonePolicy(a.policy),
|
|
768
|
+
...(a.recoveryMerges === undefined
|
|
769
|
+
? {}
|
|
770
|
+
: { recoveryMerges: a.recoveryMerges.map((entry) => ({ ...entry })) }),
|
|
752
771
|
// Written out even when it is the default, so an operator amending the
|
|
753
772
|
// volume has a line in the file to point at. Opting into availability makes
|
|
754
773
|
// the schedule explicit and daily; omitting it preserves the preset's
|
|
@@ -918,6 +937,8 @@ export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
|
918
937
|
cloneUrl: r.cloneUrl,
|
|
919
938
|
defaultBranch: r.defaultBranch,
|
|
920
939
|
gates: r.gates.map((g) => ({ cmd: g.cmd, cwd: g.cwd })),
|
|
940
|
+
...(r.migrations === undefined ? {} : { migrations: { ...r.migrations } }),
|
|
941
|
+
...(r.release === undefined ? {} : { release: { ...r.release } }),
|
|
921
942
|
})),
|
|
922
943
|
caps: { ...p.caps },
|
|
923
944
|
fallbackToIssueComment: p.escalation.fallbackToIssueComment,
|
|
@@ -938,6 +959,9 @@ export function answersFromProject(p: ProjectConfig): SetupAnswers {
|
|
|
938
959
|
if (p.workerModel !== undefined) answers.workerModel = p.workerModel;
|
|
939
960
|
if (p.escalation.telegramChatId !== undefined) answers.telegramChatId = p.escalation.telegramChatId;
|
|
940
961
|
if (p.escalation.telegramTopicId !== undefined) answers.telegramTopicId = p.escalation.telegramTopicId;
|
|
962
|
+
if (p.recoveryMerges !== undefined) {
|
|
963
|
+
answers.recoveryMerges = p.recoveryMerges.map((entry) => ({ ...entry }));
|
|
964
|
+
}
|
|
941
965
|
if (p.reporting?.digest.at !== undefined) answers.dailyDigestAt = p.reporting.digest.at;
|
|
942
966
|
if (p.reporting?.digest.timezone !== undefined) {
|
|
943
967
|
answers.reportingTimezone = p.reporting.digest.timezone;
|
package/src/store.ts
CHANGED
|
@@ -864,11 +864,15 @@ export function openStore(dbPath: string): Store {
|
|
|
864
864
|
|
|
865
865
|
const db = new Database(dbPath, { create: true });
|
|
866
866
|
|
|
867
|
+
// Set the wait before *any* pragma that may need SQLite's writer lock.
|
|
868
|
+
// journal_mode itself can return SQLITE_BUSY during another connection's
|
|
869
|
+
// close/checkpoint; setting this afterwards caused load-only CLI flakes
|
|
870
|
+
// before the schema path ever had a chance to use the timeout (#396).
|
|
871
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
867
872
|
// WAL lets the plugin read status while the daemon is mid-write; the busy
|
|
868
873
|
// timeout covers the single writer lock they still contend for.
|
|
869
874
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
870
875
|
db.exec("PRAGMA foreign_keys = ON;");
|
|
871
|
-
db.exec("PRAGMA busy_timeout = 5000;");
|
|
872
876
|
db.exec(SCHEMA);
|
|
873
877
|
// #274: a terminally failed digest did not count as sent, but the old unique
|
|
874
878
|
// index still blocked a replacement under the same daily key. Replace that
|
package/src/types.ts
CHANGED
|
@@ -125,6 +125,13 @@ export interface RepoTarget {
|
|
|
125
125
|
* = no chain check.
|
|
126
126
|
*/
|
|
127
127
|
migrations?: { dir: string };
|
|
128
|
+
/**
|
|
129
|
+
* Repository-native release preparation. `versionFile` is a repo-relative
|
|
130
|
+
* JSON file with a top-level string `version` (for example
|
|
131
|
+
* `omp/package.json`). A delegated tag is refused until that live value
|
|
132
|
+
* matches the requested tag; `version-bump-pr` is the mediated review path.
|
|
133
|
+
*/
|
|
134
|
+
release?: { versionFile: string };
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
/**
|
|
@@ -237,6 +244,7 @@ export const DEFAULT_REPORT_POLICY: ReportingPolicy = {
|
|
|
237
244
|
* dead config nor be asked about them by the wizard.
|
|
238
245
|
*/
|
|
239
246
|
export const RELEASE_SHAPES = [
|
|
247
|
+
"version-bump-pr",
|
|
240
248
|
"git-tag",
|
|
241
249
|
"git-push-tags",
|
|
242
250
|
"package-publish",
|
|
@@ -279,6 +287,7 @@ export const LEGACY_RELEASE_POLICIES = ["none", "operator-brief"] as const;
|
|
|
279
287
|
* and `undefined !== role` is only accidentally safe.
|
|
280
288
|
*/
|
|
281
289
|
export const DENIED_RELEASE_GRANTS: ResolvedGrants = {
|
|
290
|
+
"version-bump-pr": "human",
|
|
282
291
|
"git-tag": "human",
|
|
283
292
|
"git-push-tags": "human",
|
|
284
293
|
"package-publish": "human",
|
|
@@ -293,6 +302,7 @@ export const DENIED_RELEASE_GRANTS: ResolvedGrants = {
|
|
|
293
302
|
* gave — see #122.
|
|
294
303
|
*/
|
|
295
304
|
export const OPERATOR_BRIEF_GRANTS: ResolvedGrants = {
|
|
305
|
+
"version-bump-pr": "orchestrator",
|
|
296
306
|
"git-tag": "orchestrator",
|
|
297
307
|
"git-push-tags": "orchestrator",
|
|
298
308
|
"package-publish": "orchestrator",
|
|
@@ -492,6 +502,21 @@ export const MERGE_REASONS = [
|
|
|
492
502
|
|
|
493
503
|
export type MergeReason = (typeof MERGE_REASONS)[number];
|
|
494
504
|
|
|
505
|
+
/**
|
|
506
|
+
* A one-PR provenance exception an operator placed in config for recovery.
|
|
507
|
+
*
|
|
508
|
+
* It replaces broad standing merge authority for this exact request and lets
|
|
509
|
+
* an orchestrator present an otherwise-unrecorded PR to the ordinary merge
|
|
510
|
+
* gate, including while dispatch is held. It does not relax the normal merge
|
|
511
|
+
* checks. Exact URL, head and reason binding keeps the exception narrower
|
|
512
|
+
* than the action it authorizes (#382).
|
|
513
|
+
*/
|
|
514
|
+
export interface RecoveryMergeAuthorization {
|
|
515
|
+
prUrl: string;
|
|
516
|
+
headSha: string;
|
|
517
|
+
reason: Extract<MergeReason, "operator-instructed">;
|
|
518
|
+
}
|
|
519
|
+
|
|
495
520
|
/** Why a release is being cut now. Closed for the reason {@link MERGE_REASONS} is. */
|
|
496
521
|
export const RELEASE_REASONS = [
|
|
497
522
|
/** The batching unit the operator described in POLICY.md has been reached. */
|
|
@@ -622,6 +647,13 @@ export interface ProjectConfig {
|
|
|
622
647
|
* reaching for `.merge` on a project some test hand-built.
|
|
623
648
|
*/
|
|
624
649
|
policy?: ProjectPolicy;
|
|
650
|
+
/**
|
|
651
|
+
* Exact operator-authored exceptions for recovery PRs that no run recorded.
|
|
652
|
+
* Optional and hand-edited: setup preserves entries but never invents them.
|
|
653
|
+
* Each entry still passes routing, live-head, checks, migration-chain, and
|
|
654
|
+
* single-flight gates.
|
|
655
|
+
*/
|
|
656
|
+
recoveryMerges?: RecoveryMergeAuthorization[];
|
|
625
657
|
/**
|
|
626
658
|
* How loud the orchestrator is, and when the daily rollup happens. Optional
|
|
627
659
|
* on disk — a config written before this key existed loads as
|
|
@@ -1836,6 +1868,8 @@ export const VERB_REFUSALS = [
|
|
|
1836
1868
|
"open-pr-lookup-error",
|
|
1837
1869
|
/** The named pull request is not this run's, or not this project's. */
|
|
1838
1870
|
"pr-not-this-run",
|
|
1871
|
+
/** An unrecorded recovery PR was authorized, but not for these exact inputs. */
|
|
1872
|
+
"recovery-authorization-mismatch",
|
|
1839
1873
|
/** The run has no pull request to act on. */
|
|
1840
1874
|
"pr-missing",
|
|
1841
1875
|
/**
|