omp-conductor 0.17.1 → 0.18.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/README.md +34 -0
- package/REFERENCE.md +71 -17
- package/agents/to-spec.md +90 -0
- package/package.json +2 -1
- package/schema/config.schema.json +53 -1
- package/src/admission.ts +308 -76
- package/src/ask.ts +307 -10
- package/src/backups.ts +2 -2
- package/src/board.ts +17 -3
- package/src/briefs/orchestrator.md +43 -14
- package/src/briefs/to-spec.md +84 -0
- package/src/briefs/worker.md +37 -19
- package/src/cli.ts +2 -0
- package/src/command-help.ts +19 -1
- package/src/command-manifest.ts +27 -2
- package/src/commands/context.ts +1 -0
- package/src/commands/drain.ts +176 -0
- package/src/commands/extend.ts +6 -10
- package/src/commands/status.ts +5 -1
- package/src/commands/watch.ts +110 -3
- package/src/commands/worker.ts +9 -10
- package/src/config-schema.ts +57 -0
- package/src/config.ts +102 -2
- package/src/daemon.ts +1220 -1517
- package/src/dashboard/app.js +4 -1
- package/src/dashboard/server.ts +5 -2
- package/src/decisions.ts +279 -16
- package/src/depends-on.ts +261 -1
- package/src/diff-flags.ts +425 -1
- package/src/digest-schedule.ts +37 -0
- package/src/doctor.ts +52 -0
- package/src/escalate.ts +9 -3
- package/src/failure-class.ts +43 -4
- package/src/fleet.ts +166 -24
- package/src/gitops.ts +188 -81
- package/src/graph-health.ts +55 -8
- package/src/graph.ts +379 -69
- package/src/harness-loader.ts +59 -0
- package/src/host.ts +567 -2
- package/src/lifecycle.ts +158 -6
- package/src/omp.ts +269 -20
- package/src/orchestrator-tick.ts +1489 -26
- package/src/orchestrator.ts +12 -0
- package/src/privileged.ts +1 -4
- package/src/release-policy.ts +503 -9
- package/src/routing.ts +11 -3
- package/src/session-host.ts +115 -5
- package/src/settlement.ts +1780 -0
- package/src/setup-host.ts +1205 -6
- package/src/setup-install.ts +119 -30
- package/src/setup-wizard.ts +88 -2
- package/src/setup.ts +119 -13
- package/src/shell.ts +15 -0
- package/src/status-render.ts +100 -11
- package/src/store.ts +519 -45
- package/src/to-spec.ts +387 -0
- package/src/tracker/github.ts +150 -14
- package/src/types.ts +470 -16
- package/src/upgrade-verify.ts +209 -2
- package/src/upgrade.ts +175 -1
- package/src/verbs/protocol.ts +39 -0
- package/src/verbs/server.ts +770 -40
- package/src/verbs/socket.ts +24 -5
- package/src/worker.ts +239 -9
- package/src/worktree.ts +142 -18
package/src/config-schema.ts
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
DEFAULT_AUTHORITY,
|
|
32
32
|
DEFAULT_CAPS,
|
|
33
33
|
DEFAULT_PROJECT_POLICY,
|
|
34
|
+
DEFAULT_REVIEW_MAX_ROUNDS,
|
|
35
|
+
DEFAULT_REVIEW_STRICTNESS,
|
|
34
36
|
DRAFT_POLICIES,
|
|
35
37
|
INTERRUPT_CATEGORIES,
|
|
36
38
|
LEGACY_RELEASE_POLICIES,
|
|
@@ -39,6 +41,9 @@ import {
|
|
|
39
41
|
RELEASE_REQUIREMENTS,
|
|
40
42
|
RELEASE_SHAPES,
|
|
41
43
|
REPORT_SCOPES,
|
|
44
|
+
REVIEW_MAX_ROUNDS_MAX,
|
|
45
|
+
REVIEW_MAX_ROUNDS_MIN,
|
|
46
|
+
REVIEW_STRICTNESS,
|
|
42
47
|
WEEKDAYS,
|
|
43
48
|
DIGEST_CADENCES,
|
|
44
49
|
} from "./types.ts";
|
|
@@ -65,6 +70,7 @@ const RELEASE_REQUIREMENT_LIST = quoteList(RELEASE_REQUIREMENTS);
|
|
|
65
70
|
const RELEASE_SHAPE_LIST = quoteList(RELEASE_SHAPES);
|
|
66
71
|
const ORCHESTRATOR_MODE_LIST = quoteList(ORCHESTRATOR_MODES);
|
|
67
72
|
const LEGACY_RELEASE_POLICY_LIST = quoteList(LEGACY_RELEASE_POLICIES);
|
|
73
|
+
const REVIEW_STRICTNESS_LIST = quoteList(REVIEW_STRICTNESS);
|
|
68
74
|
|
|
69
75
|
// ---------------------------------------------------------------------------
|
|
70
76
|
// Closed vocabularies — every one built from the exported `as const` array in
|
|
@@ -83,6 +89,7 @@ const releaseRequirementEnum = z.enum([...RELEASE_REQUIREMENTS]);
|
|
|
83
89
|
const releaseShapeEnum = z.enum([...RELEASE_SHAPES]);
|
|
84
90
|
const orchestratorModeEnum = z.enum([...ORCHESTRATOR_MODES]);
|
|
85
91
|
const releasePolicyLegacyEnum = z.enum([...LEGACY_RELEASE_POLICIES]);
|
|
92
|
+
const reviewStrictnessEnum = z.enum([...REVIEW_STRICTNESS]);
|
|
86
93
|
|
|
87
94
|
/** The 24-hour `HH:MM` shape `digest.at` / `availability.start/end` take. */
|
|
88
95
|
const HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
@@ -256,6 +263,27 @@ const armSchema = z
|
|
|
256
263
|
.strict()
|
|
257
264
|
.describe("How `arm` proves a human just approved arming");
|
|
258
265
|
|
|
266
|
+
/**
|
|
267
|
+
* The per-project review policy (#678): how strictly green PRs are reviewed
|
|
268
|
+
* and returned, and the hard ceiling on review rounds per PR lifecycle. Absent
|
|
269
|
+
* or a legacy config without the key loads as `medium` with
|
|
270
|
+
* {@link DEFAULT_REVIEW_MAX_ROUNDS} rounds — the recommended default for a new
|
|
271
|
+
* project, materialised deterministically for every existing one, with the
|
|
272
|
+
* schema bounds and the runtime defaults read from the same constants.
|
|
273
|
+
*/
|
|
274
|
+
const reviewSchema = z
|
|
275
|
+
.object({
|
|
276
|
+
strictness: reviewStrictnessEnum.default(DEFAULT_REVIEW_STRICTNESS),
|
|
277
|
+
maxRounds: z
|
|
278
|
+
.number()
|
|
279
|
+
.int()
|
|
280
|
+
.min(REVIEW_MAX_ROUNDS_MIN)
|
|
281
|
+
.max(REVIEW_MAX_ROUNDS_MAX)
|
|
282
|
+
.default(DEFAULT_REVIEW_MAX_ROUNDS),
|
|
283
|
+
})
|
|
284
|
+
.strict()
|
|
285
|
+
.describe("Review strictness and round ceiling for green PRs");
|
|
286
|
+
|
|
259
287
|
const releasePolicySchema = z.union([
|
|
260
288
|
releasePolicyLegacyEnum,
|
|
261
289
|
z.record(z.string(), authorityHolderEnum),
|
|
@@ -304,9 +332,29 @@ const stateLabelsSchema = z
|
|
|
304
332
|
inProgress: z.unknown(),
|
|
305
333
|
blocked: z.unknown(),
|
|
306
334
|
failed: z.unknown(),
|
|
335
|
+
backlog: z.unknown(),
|
|
307
336
|
})
|
|
308
337
|
.partial();
|
|
309
338
|
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Host constraints (#721)
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
const hostConstraintsSchema = z
|
|
344
|
+
.object({
|
|
345
|
+
description: z.string().min(1).describe("What this host is and what else it runs"),
|
|
346
|
+
path: z
|
|
347
|
+
.string()
|
|
348
|
+
.min(1)
|
|
349
|
+
.describe('The non-interactive PATH a script or `ssh host "<cmd>"` invocation must export'),
|
|
350
|
+
conventions: z
|
|
351
|
+
.record(z.string().min(1), z.string().min(1))
|
|
352
|
+
.describe("Per-repo command conventions, keyed by the brief's `owner/repo` slug"),
|
|
353
|
+
})
|
|
354
|
+
.strict()
|
|
355
|
+
.partial()
|
|
356
|
+
.describe("Operator-authored host facts rendered into every worker brief");
|
|
357
|
+
|
|
310
358
|
// ---------------------------------------------------------------------------
|
|
311
359
|
// Project / root
|
|
312
360
|
// ---------------------------------------------------------------------------
|
|
@@ -350,6 +398,9 @@ const projectSchema = z
|
|
|
350
398
|
// How `arm` proves a human approved arming (#613); absent loads as
|
|
351
399
|
// `challenge`, preserving today's authenticated round-trip.
|
|
352
400
|
arm: armSchema.optional(),
|
|
401
|
+
// Review strictness and the round ceiling (#678); absent loads as the
|
|
402
|
+
// documented migration default.
|
|
403
|
+
review: reviewSchema.optional(),
|
|
353
404
|
authority: authoritySchema.optional(),
|
|
354
405
|
releasePolicy: releasePolicySchema.optional(),
|
|
355
406
|
policy: projectPolicySchema.optional(),
|
|
@@ -377,6 +428,11 @@ const configSchema = z
|
|
|
377
428
|
.min(1, "must name a directory")
|
|
378
429
|
.optional()
|
|
379
430
|
.describe("Absolute directory for restorable conductor.db snapshots; defaults to <stateDir>/backups/db"),
|
|
431
|
+
// Cores and RAM are derived by host.ts at render time; only what the
|
|
432
|
+
// operator must type belongs in this block.
|
|
433
|
+
host: hostConstraintsSchema
|
|
434
|
+
.optional()
|
|
435
|
+
.describe("Host facts every worker brief renders; absent renders no section"),
|
|
380
436
|
projects: z.array(projectSchema).min(1, `"projects" must be a non-empty array — the dispatcher has nothing to service otherwise`),
|
|
381
437
|
})
|
|
382
438
|
.loose()
|
|
@@ -448,4 +504,5 @@ export {
|
|
|
448
504
|
RELEASE_SHAPE_LIST,
|
|
449
505
|
ORCHESTRATOR_MODE_LIST,
|
|
450
506
|
LEGACY_RELEASE_POLICY_LIST,
|
|
507
|
+
REVIEW_STRICTNESS_LIST,
|
|
451
508
|
};
|
package/src/config.ts
CHANGED
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
DEFAULT_PROJECT_POLICY,
|
|
29
29
|
DEFAULT_REPORT_POLICY,
|
|
30
30
|
DEFAULT_REPORT_SCOPE,
|
|
31
|
+
DEFAULT_REVIEW_MAX_ROUNDS,
|
|
32
|
+
DEFAULT_REVIEW_POLICY,
|
|
33
|
+
DEFAULT_REVIEW_STRICTNESS,
|
|
31
34
|
DENIED_RELEASE_GRANTS,
|
|
32
35
|
DRAFT_POLICIES,
|
|
33
36
|
LEGACY_RELEASE_POLICIES,
|
|
@@ -37,12 +40,16 @@ import {
|
|
|
37
40
|
RELEASE_REQUIREMENTS,
|
|
38
41
|
RELEASE_SHAPES,
|
|
39
42
|
REPORT_SCOPES,
|
|
43
|
+
REVIEW_MAX_ROUNDS_MAX,
|
|
44
|
+
REVIEW_MAX_ROUNDS_MIN,
|
|
45
|
+
REVIEW_STRICTNESS,
|
|
40
46
|
INTERRUPT_CATEGORIES,
|
|
41
47
|
DIGEST_CADENCES,
|
|
42
48
|
WEEKDAYS,
|
|
43
49
|
type Caps,
|
|
44
50
|
type ConductorConfig,
|
|
45
51
|
type DigestCadence,
|
|
52
|
+
type HostConstraints,
|
|
46
53
|
type InterruptCategory,
|
|
47
54
|
type MergePreconditions,
|
|
48
55
|
type PlanUsageCap,
|
|
@@ -53,6 +60,8 @@ import {
|
|
|
53
60
|
type ReleaseRequirement,
|
|
54
61
|
type ReportScope,
|
|
55
62
|
type ReportingPolicy,
|
|
63
|
+
type ReviewPolicy,
|
|
64
|
+
type ReviewStrictness,
|
|
56
65
|
type Weekday,
|
|
57
66
|
type WeeklyAvailability,
|
|
58
67
|
type RepoTarget,
|
|
@@ -71,6 +80,7 @@ import {
|
|
|
71
80
|
ORCHESTRATOR_MODE_LIST,
|
|
72
81
|
RELEASE_REQUIREMENT_LIST,
|
|
73
82
|
RELEASE_SHAPE_LIST,
|
|
83
|
+
REVIEW_STRICTNESS_LIST,
|
|
74
84
|
REPORT_SCOPE_LIST,
|
|
75
85
|
WEEKDAY_LIST,
|
|
76
86
|
quoteList,
|
|
@@ -98,10 +108,13 @@ const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
|
98
108
|
* Used when a project omits `stateLabels`. Namespaced so a human scanning the
|
|
99
109
|
* tracker can tell dispatcher-written labels from their own.
|
|
100
110
|
*/
|
|
101
|
-
const DEFAULT_STATE_LABELS: ProjectConfig["stateLabels"] = {
|
|
111
|
+
export const DEFAULT_STATE_LABELS: ProjectConfig["stateLabels"] = {
|
|
102
112
|
inProgress: "agent:in-progress",
|
|
103
113
|
blocked: "agent:blocked",
|
|
104
114
|
failed: "agent:failed",
|
|
115
|
+
// The operator's park gesture (#507): unnamed by design — it is theirs, and
|
|
116
|
+
// the operator adds it by hand until setup grows a confirm step (slice 2).
|
|
117
|
+
backlog: "backlog",
|
|
105
118
|
};
|
|
106
119
|
|
|
107
120
|
/**
|
|
@@ -330,6 +343,20 @@ export function resolveArmProof(p: ProjectConfig): ArmProof {
|
|
|
330
343
|
return p.arm?.proof === "claim-only" ? "claim-only" : DEFAULT_ARM_PROOF;
|
|
331
344
|
}
|
|
332
345
|
|
|
346
|
+
/**
|
|
347
|
+
* The project's review policy (#678), complete.
|
|
348
|
+
*
|
|
349
|
+
* The loader always materialises `review` (finalizeProject), so this is only
|
|
350
|
+
* ever the fallback for a `ProjectConfig` that never went through it — a
|
|
351
|
+
* hand-built one in a test, or a config written before the key existed.
|
|
352
|
+
* Absent resolves to {@link DEFAULT_REVIEW_POLICY}, the documented migration
|
|
353
|
+
* default every existing install deterministically upgrades to. A fresh object
|
|
354
|
+
* each call, so mutating the result cannot reach the config it came from.
|
|
355
|
+
*/
|
|
356
|
+
export function resolveReview(p: ProjectConfig): ReviewPolicy {
|
|
357
|
+
return { ...(p.review ?? DEFAULT_REVIEW_POLICY) };
|
|
358
|
+
}
|
|
359
|
+
|
|
333
360
|
/**
|
|
334
361
|
* A policy with no array shared with its source.
|
|
335
362
|
*
|
|
@@ -861,6 +888,12 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
|
|
|
861
888
|
if (rel.length === 1 && relStr === "policy") {
|
|
862
889
|
return `policy must be an object with "merge" and "release" sections`;
|
|
863
890
|
}
|
|
891
|
+
if (rel.length === 1 && relStr === "review") {
|
|
892
|
+
return `review must be an object with "strictness" and "maxRounds"`;
|
|
893
|
+
}
|
|
894
|
+
if (rel.length === 2 && rel[0] === "review" && rel[1] === "maxRounds") {
|
|
895
|
+
return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
|
|
896
|
+
}
|
|
864
897
|
if (rel.length === 2 && relStr === "policy") return `${pathStr} must be an object`;
|
|
865
898
|
if (rel.length === 1 && relStr === "caps") return `caps must be an object`;
|
|
866
899
|
if (rel.length === 2 && relStr === "caps" && rel[1] === "planUsage") {
|
|
@@ -911,6 +944,7 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
|
|
|
911
944
|
if (issue.code === "unrecognized_keys") {
|
|
912
945
|
const keys = (issue.keys ?? []).join(", ");
|
|
913
946
|
if (pathStr === "policy") return `policy has unknown key(s): ${keys} — expected "merge" or "release"`;
|
|
947
|
+
if (pathStr === "review") return `review has unknown key(s): ${keys} — expected "strictness" and "maxRounds"`;
|
|
914
948
|
if (pathStr === "policy.merge") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_MERGE_KEYS}`;
|
|
915
949
|
if (pathStr === "policy.release") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_RELEASE_KEYS}`;
|
|
916
950
|
if (pathStr === "caps.planUsage") {
|
|
@@ -960,6 +994,9 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
|
|
|
960
994
|
|
|
961
995
|
// Regex / custom-message failures carry the finding in issue.message.
|
|
962
996
|
if (issue.code === "too_small") {
|
|
997
|
+
if (pathStr === "review.maxRounds") {
|
|
998
|
+
return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
|
|
999
|
+
}
|
|
963
1000
|
// Empty array (reporting.interruptOn, availability.days) has its own line.
|
|
964
1001
|
if (parentOf(rel) === "reporting.interruptOn") return `reporting.interruptOn must name at least one category`;
|
|
965
1002
|
if (pathStr === "reporting.availability.days") return `reporting.availability.days must be a non-empty array of ${WEEKDAY_LIST}`;
|
|
@@ -975,6 +1012,9 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
|
|
|
975
1012
|
}
|
|
976
1013
|
|
|
977
1014
|
if (issue.code === "too_big") {
|
|
1015
|
+
if (pathStr === "review.maxRounds") {
|
|
1016
|
+
return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
|
|
1017
|
+
}
|
|
978
1018
|
if (rel.length === 2 && rel[0] === "caps") return capProblem(rel[1] as string, found());
|
|
979
1019
|
if (pathStr === "caps.planUsage.maxUsedFraction") {
|
|
980
1020
|
return `caps.planUsage.maxUsedFraction must be a fraction between 0 and 1 (0.85 holds at 85% of the allowance), found ${found()}`;
|
|
@@ -1053,8 +1093,48 @@ function finalize(data: unknown, path: string): ConductorConfig {
|
|
|
1053
1093
|
});
|
|
1054
1094
|
}
|
|
1055
1095
|
|
|
1096
|
+
const rawHost = root["host"] as Raw | undefined;
|
|
1097
|
+
const host = finalizeHost(rawHost);
|
|
1098
|
+
const rawDbBackupDir = root["dbBackupDir"];
|
|
1099
|
+
const dbBackupDir = typeof rawDbBackupDir === "string" ? rawDbBackupDir : undefined;
|
|
1100
|
+
|
|
1056
1101
|
if (problems.length > 0) throw new Error(problemEnvelope(path, problems));
|
|
1057
|
-
|
|
1102
|
+
// `ConductorConfig` is rebuilt here, so every optional top-level key must be
|
|
1103
|
+
// spelled out or it silently vanishes from every loaded config. `dbBackupDir`
|
|
1104
|
+
// and `host` are the two today; a third needs its own line below (#773).
|
|
1105
|
+
return {
|
|
1106
|
+
version: CONFIG_VERSION,
|
|
1107
|
+
defaults,
|
|
1108
|
+
projects,
|
|
1109
|
+
...(dbBackupDir === undefined ? {} : { dbBackupDir }),
|
|
1110
|
+
...(host === undefined ? {} : { host }),
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* The typed host-constraints block (#721), or `undefined` when nothing usable
|
|
1116
|
+
* was configured — matching the schema's "absent renders no section" promise.
|
|
1117
|
+
* Trimming and empty-drop mirror the other hand-edited string fields; an
|
|
1118
|
+
* object whose every key is empty loads as if the field were absent.
|
|
1119
|
+
*/
|
|
1120
|
+
function finalizeHost(parsed: Raw | undefined): HostConstraints | undefined {
|
|
1121
|
+
if (parsed === undefined) return undefined;
|
|
1122
|
+
const out: HostConstraints = {};
|
|
1123
|
+
const description = parsed["description"];
|
|
1124
|
+
if (typeof description === "string" && description.trim() !== "") out.description = description.trim();
|
|
1125
|
+
const path = parsed["path"];
|
|
1126
|
+
if (typeof path === "string" && path.trim() !== "") out.path = path.trim();
|
|
1127
|
+
const conventions = parsed["conventions"];
|
|
1128
|
+
if (typeof conventions === "object" && conventions !== null && !Array.isArray(conventions)) {
|
|
1129
|
+
const kept: Record<string, string> = {};
|
|
1130
|
+
for (const [slug, value] of Object.entries(conventions as Record<string, unknown>)) {
|
|
1131
|
+
if (typeof value === "string" && value.trim() !== "" && slug.trim() !== "") {
|
|
1132
|
+
kept[slug.trim()] = value.trim();
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (Object.keys(kept).length > 0) out.conventions = kept;
|
|
1136
|
+
}
|
|
1137
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
1058
1138
|
}
|
|
1059
1139
|
|
|
1060
1140
|
function finalizeProject(
|
|
@@ -1101,6 +1181,7 @@ function finalizeProject(
|
|
|
1101
1181
|
);
|
|
1102
1182
|
}
|
|
1103
1183
|
const policy = finalizePolicy(p["policy"], label, problems);
|
|
1184
|
+
const review = finalizeReview(p["review"] as Raw | undefined);
|
|
1104
1185
|
const caps = reconcileCaps(p["caps"], `${label}: caps`, problems, legacyCaps);
|
|
1105
1186
|
const reporting = finalizeReporting(p["reporting"], label, problems);
|
|
1106
1187
|
const recoveryMerges = (p["recoveryMerges"] as RecoveryMergeAuthorization[] | undefined)?.map(
|
|
@@ -1199,6 +1280,7 @@ function finalizeProject(
|
|
|
1199
1280
|
inProgress: pickString(stateLabels?.["inProgress"], DEFAULT_STATE_LABELS.inProgress),
|
|
1200
1281
|
blocked: pickString(stateLabels?.["blocked"], DEFAULT_STATE_LABELS.blocked),
|
|
1201
1282
|
failed: pickString(stateLabels?.["failed"], DEFAULT_STATE_LABELS.failed),
|
|
1283
|
+
backlog: pickString(stateLabels?.["backlog"], DEFAULT_STATE_LABELS.backlog),
|
|
1202
1284
|
},
|
|
1203
1285
|
routing: { labelPrefix, repos },
|
|
1204
1286
|
caps,
|
|
@@ -1208,6 +1290,7 @@ function finalizeProject(
|
|
|
1208
1290
|
...(effectiveOmpSettings === undefined ? {} : { ompSettings: effectiveOmpSettings }),
|
|
1209
1291
|
escalation,
|
|
1210
1292
|
arm,
|
|
1293
|
+
review,
|
|
1211
1294
|
authority,
|
|
1212
1295
|
releasePolicy,
|
|
1213
1296
|
policy,
|
|
@@ -1231,6 +1314,23 @@ function finalizeArm(parsed: Raw | undefined): ProjectConfig["arm"] {
|
|
|
1231
1314
|
return { proof: (parsed["proof"] as ArmProof | undefined) ?? DEFAULT_ARM_PROOF };
|
|
1232
1315
|
}
|
|
1233
1316
|
|
|
1317
|
+
/**
|
|
1318
|
+
* The project's review policy (#678), complete. A config written before the
|
|
1319
|
+
* key existed — or one that never answered — materialises as
|
|
1320
|
+
* {@link DEFAULT_REVIEW_POLICY}, the documented migration default, so an
|
|
1321
|
+
* upgrade changes no existing project's behaviour beyond what its operators
|
|
1322
|
+
* then choose in setup. zod has already rejected anything outside the
|
|
1323
|
+
* strictness vocabulary or the validated round range, so this only fills the
|
|
1324
|
+
* absent-case default.
|
|
1325
|
+
*/
|
|
1326
|
+
function finalizeReview(parsed: Raw | undefined): ProjectConfig["review"] {
|
|
1327
|
+
if (parsed === undefined) return { ...DEFAULT_REVIEW_POLICY };
|
|
1328
|
+
return {
|
|
1329
|
+
strictness: (parsed["strictness"] as ReviewStrictness | undefined) ?? DEFAULT_REVIEW_STRICTNESS,
|
|
1330
|
+
maxRounds: (parsed["maxRounds"] as number | undefined) ?? DEFAULT_REVIEW_MAX_ROUNDS,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1234
1334
|
function finalizeEscalation(parsed: Raw | undefined): ProjectConfig["escalation"] {
|
|
1235
1335
|
if (parsed === undefined) {
|
|
1236
1336
|
return { fallbackToIssueComment: true, orchestrator: "embedded" };
|