omp-conductor 0.17.1 → 0.18.0

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/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,6 +40,9 @@ 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,
@@ -53,6 +59,8 @@ import {
53
59
  type ReleaseRequirement,
54
60
  type ReportScope,
55
61
  type ReportingPolicy,
62
+ type ReviewPolicy,
63
+ type ReviewStrictness,
56
64
  type Weekday,
57
65
  type WeeklyAvailability,
58
66
  type RepoTarget,
@@ -71,6 +79,7 @@ import {
71
79
  ORCHESTRATOR_MODE_LIST,
72
80
  RELEASE_REQUIREMENT_LIST,
73
81
  RELEASE_SHAPE_LIST,
82
+ REVIEW_STRICTNESS_LIST,
74
83
  REPORT_SCOPE_LIST,
75
84
  WEEKDAY_LIST,
76
85
  quoteList,
@@ -98,10 +107,13 @@ const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
98
107
  * Used when a project omits `stateLabels`. Namespaced so a human scanning the
99
108
  * tracker can tell dispatcher-written labels from their own.
100
109
  */
101
- const DEFAULT_STATE_LABELS: ProjectConfig["stateLabels"] = {
110
+ export const DEFAULT_STATE_LABELS: ProjectConfig["stateLabels"] = {
102
111
  inProgress: "agent:in-progress",
103
112
  blocked: "agent:blocked",
104
113
  failed: "agent:failed",
114
+ // The operator's park gesture (#507): unnamed by design — it is theirs, and
115
+ // the operator adds it by hand until setup grows a confirm step (slice 2).
116
+ backlog: "backlog",
105
117
  };
106
118
 
107
119
  /**
@@ -330,6 +342,20 @@ export function resolveArmProof(p: ProjectConfig): ArmProof {
330
342
  return p.arm?.proof === "claim-only" ? "claim-only" : DEFAULT_ARM_PROOF;
331
343
  }
332
344
 
345
+ /**
346
+ * The project's review policy (#678), complete.
347
+ *
348
+ * The loader always materialises `review` (finalizeProject), so this is only
349
+ * ever the fallback for a `ProjectConfig` that never went through it — a
350
+ * hand-built one in a test, or a config written before the key existed.
351
+ * Absent resolves to {@link DEFAULT_REVIEW_POLICY}, the documented migration
352
+ * default every existing install deterministically upgrades to. A fresh object
353
+ * each call, so mutating the result cannot reach the config it came from.
354
+ */
355
+ export function resolveReview(p: ProjectConfig): ReviewPolicy {
356
+ return { ...(p.review ?? DEFAULT_REVIEW_POLICY) };
357
+ }
358
+
333
359
  /**
334
360
  * A policy with no array shared with its source.
335
361
  *
@@ -861,6 +887,12 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
861
887
  if (rel.length === 1 && relStr === "policy") {
862
888
  return `policy must be an object with "merge" and "release" sections`;
863
889
  }
890
+ if (rel.length === 1 && relStr === "review") {
891
+ return `review must be an object with "strictness" and "maxRounds"`;
892
+ }
893
+ if (rel.length === 2 && rel[0] === "review" && rel[1] === "maxRounds") {
894
+ return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
895
+ }
864
896
  if (rel.length === 2 && relStr === "policy") return `${pathStr} must be an object`;
865
897
  if (rel.length === 1 && relStr === "caps") return `caps must be an object`;
866
898
  if (rel.length === 2 && relStr === "caps" && rel[1] === "planUsage") {
@@ -911,6 +943,7 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
911
943
  if (issue.code === "unrecognized_keys") {
912
944
  const keys = (issue.keys ?? []).join(", ");
913
945
  if (pathStr === "policy") return `policy has unknown key(s): ${keys} — expected "merge" or "release"`;
946
+ if (pathStr === "review") return `review has unknown key(s): ${keys} — expected "strictness" and "maxRounds"`;
914
947
  if (pathStr === "policy.merge") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_MERGE_KEYS}`;
915
948
  if (pathStr === "policy.release") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_RELEASE_KEYS}`;
916
949
  if (pathStr === "caps.planUsage") {
@@ -960,6 +993,9 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
960
993
 
961
994
  // Regex / custom-message failures carry the finding in issue.message.
962
995
  if (issue.code === "too_small") {
996
+ if (pathStr === "review.maxRounds") {
997
+ return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
998
+ }
963
999
  // Empty array (reporting.interruptOn, availability.days) has its own line.
964
1000
  if (parentOf(rel) === "reporting.interruptOn") return `reporting.interruptOn must name at least one category`;
965
1001
  if (pathStr === "reporting.availability.days") return `reporting.availability.days must be a non-empty array of ${WEEKDAY_LIST}`;
@@ -975,6 +1011,9 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
975
1011
  }
976
1012
 
977
1013
  if (issue.code === "too_big") {
1014
+ if (pathStr === "review.maxRounds") {
1015
+ return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
1016
+ }
978
1017
  if (rel.length === 2 && rel[0] === "caps") return capProblem(rel[1] as string, found());
979
1018
  if (pathStr === "caps.planUsage.maxUsedFraction") {
980
1019
  return `caps.planUsage.maxUsedFraction must be a fraction between 0 and 1 (0.85 holds at 85% of the allowance), found ${found()}`;
@@ -1101,6 +1140,7 @@ function finalizeProject(
1101
1140
  );
1102
1141
  }
1103
1142
  const policy = finalizePolicy(p["policy"], label, problems);
1143
+ const review = finalizeReview(p["review"] as Raw | undefined);
1104
1144
  const caps = reconcileCaps(p["caps"], `${label}: caps`, problems, legacyCaps);
1105
1145
  const reporting = finalizeReporting(p["reporting"], label, problems);
1106
1146
  const recoveryMerges = (p["recoveryMerges"] as RecoveryMergeAuthorization[] | undefined)?.map(
@@ -1199,6 +1239,7 @@ function finalizeProject(
1199
1239
  inProgress: pickString(stateLabels?.["inProgress"], DEFAULT_STATE_LABELS.inProgress),
1200
1240
  blocked: pickString(stateLabels?.["blocked"], DEFAULT_STATE_LABELS.blocked),
1201
1241
  failed: pickString(stateLabels?.["failed"], DEFAULT_STATE_LABELS.failed),
1242
+ backlog: pickString(stateLabels?.["backlog"], DEFAULT_STATE_LABELS.backlog),
1202
1243
  },
1203
1244
  routing: { labelPrefix, repos },
1204
1245
  caps,
@@ -1208,6 +1249,7 @@ function finalizeProject(
1208
1249
  ...(effectiveOmpSettings === undefined ? {} : { ompSettings: effectiveOmpSettings }),
1209
1250
  escalation,
1210
1251
  arm,
1252
+ review,
1211
1253
  authority,
1212
1254
  releasePolicy,
1213
1255
  policy,
@@ -1231,6 +1273,23 @@ function finalizeArm(parsed: Raw | undefined): ProjectConfig["arm"] {
1231
1273
  return { proof: (parsed["proof"] as ArmProof | undefined) ?? DEFAULT_ARM_PROOF };
1232
1274
  }
1233
1275
 
1276
+ /**
1277
+ * The project's review policy (#678), complete. A config written before the
1278
+ * key existed — or one that never answered — materialises as
1279
+ * {@link DEFAULT_REVIEW_POLICY}, the documented migration default, so an
1280
+ * upgrade changes no existing project's behaviour beyond what its operators
1281
+ * then choose in setup. zod has already rejected anything outside the
1282
+ * strictness vocabulary or the validated round range, so this only fills the
1283
+ * absent-case default.
1284
+ */
1285
+ function finalizeReview(parsed: Raw | undefined): ProjectConfig["review"] {
1286
+ if (parsed === undefined) return { ...DEFAULT_REVIEW_POLICY };
1287
+ return {
1288
+ strictness: (parsed["strictness"] as ReviewStrictness | undefined) ?? DEFAULT_REVIEW_STRICTNESS,
1289
+ maxRounds: (parsed["maxRounds"] as number | undefined) ?? DEFAULT_REVIEW_MAX_ROUNDS,
1290
+ };
1291
+ }
1292
+
1234
1293
  function finalizeEscalation(parsed: Raw | undefined): ProjectConfig["escalation"] {
1235
1294
  if (parsed === undefined) {
1236
1295
  return { fallbackToIssueComment: true, orchestrator: "embedded" };