omp-conductor 0.17.0 → 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.
Files changed (51) hide show
  1. package/REFERENCE.md +12 -8
  2. package/package.json +1 -1
  3. package/schema/config.schema.json +40 -1
  4. package/src/admission.ts +263 -44
  5. package/src/ask.ts +39 -3
  6. package/src/availability.ts +27 -1
  7. package/src/backups.ts +2 -2
  8. package/src/briefs/orchestrator.md +1 -0
  9. package/src/briefs/worker.md +38 -19
  10. package/src/command-help.ts +8 -1
  11. package/src/command-manifest.ts +5 -2
  12. package/src/commands/arm.ts +6 -3
  13. package/src/commands/message.ts +32 -4
  14. package/src/commands/watch.ts +62 -3
  15. package/src/config-schema.ts +53 -0
  16. package/src/config.ts +97 -1
  17. package/src/daemon.ts +1479 -1483
  18. package/src/decisions.ts +51 -6
  19. package/src/depends-on.ts +261 -1
  20. package/src/diff-flags.ts +350 -0
  21. package/src/digest-schedule.ts +37 -0
  22. package/src/doctor.ts +310 -22
  23. package/src/escalate.ts +560 -57
  24. package/src/failure-class.ts +71 -15
  25. package/src/fleet.ts +189 -34
  26. package/src/gitops.ts +103 -24
  27. package/src/graph-health.ts +20 -7
  28. package/src/graph.ts +313 -68
  29. package/src/lifecycle.ts +43 -7
  30. package/src/omp.ts +42 -0
  31. package/src/orchestrator-tick.ts +430 -162
  32. package/src/release-policy.ts +177 -5
  33. package/src/routing.ts +11 -3
  34. package/src/session-host.ts +16 -0
  35. package/src/settlement.ts +1728 -0
  36. package/src/setup-host.ts +193 -4
  37. package/src/setup-install.ts +91 -30
  38. package/src/setup-wizard.ts +1257 -78
  39. package/src/setup.ts +153 -6
  40. package/src/status-render.ts +36 -4
  41. package/src/store.ts +411 -17
  42. package/src/tracker/github.ts +607 -12
  43. package/src/types.ts +331 -5
  44. package/src/upgrade.ts +50 -19
  45. package/src/verbs/actions.ts +66 -18
  46. package/src/verbs/protocol.ts +45 -0
  47. package/src/verbs/server.ts +270 -13
  48. package/src/worker.ts +239 -6
  49. package/src/worktree.ts +115 -8
  50. package/systemd/omp-conductor-recover.sh +73 -0
  51. package/systemd/recover-unit-test.sh +61 -0
package/src/config.ts CHANGED
@@ -17,15 +17,20 @@ import { homedir } from "node:os";
17
17
  import { dirname, isAbsolute, join } from "node:path";
18
18
  import { backupTimestamp, copyToUniqueBackup } from "./backups.ts";
19
19
  import {
20
+ ARM_PROOFS,
20
21
  AUTHORITY_HOLDERS,
21
22
  BASE_FRESHNESS,
22
23
  BEHIND_BASE_ACTIONS,
23
24
  CONFIG_VERSION,
25
+ DEFAULT_ARM_PROOF,
24
26
  DEFAULT_AUTHORITY,
25
27
  DEFAULT_CAPS,
26
28
  DEFAULT_PROJECT_POLICY,
27
29
  DEFAULT_REPORT_POLICY,
28
30
  DEFAULT_REPORT_SCOPE,
31
+ DEFAULT_REVIEW_MAX_ROUNDS,
32
+ DEFAULT_REVIEW_POLICY,
33
+ DEFAULT_REVIEW_STRICTNESS,
29
34
  DENIED_RELEASE_GRANTS,
30
35
  DRAFT_POLICIES,
31
36
  LEGACY_RELEASE_POLICIES,
@@ -35,6 +40,9 @@ import {
35
40
  RELEASE_REQUIREMENTS,
36
41
  RELEASE_SHAPES,
37
42
  REPORT_SCOPES,
43
+ REVIEW_MAX_ROUNDS_MAX,
44
+ REVIEW_MAX_ROUNDS_MIN,
45
+ REVIEW_STRICTNESS,
38
46
  INTERRUPT_CATEGORIES,
39
47
  DIGEST_CADENCES,
40
48
  WEEKDAYS,
@@ -51,10 +59,13 @@ import {
51
59
  type ReleaseRequirement,
52
60
  type ReportScope,
53
61
  type ReportingPolicy,
62
+ type ReviewPolicy,
63
+ type ReviewStrictness,
54
64
  type Weekday,
55
65
  type WeeklyAvailability,
56
66
  type RepoTarget,
57
67
  type ResolvedGrants,
68
+ type ArmProof,
58
69
  } from "./types.ts";
59
70
  import {
60
71
  AUTHORITY_HOLDER_LIST,
@@ -68,6 +79,7 @@ import {
68
79
  ORCHESTRATOR_MODE_LIST,
69
80
  RELEASE_REQUIREMENT_LIST,
70
81
  RELEASE_SHAPE_LIST,
82
+ REVIEW_STRICTNESS_LIST,
71
83
  REPORT_SCOPE_LIST,
72
84
  WEEKDAY_LIST,
73
85
  quoteList,
@@ -95,10 +107,13 @@ const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
95
107
  * Used when a project omits `stateLabels`. Namespaced so a human scanning the
96
108
  * tracker can tell dispatcher-written labels from their own.
97
109
  */
98
- const DEFAULT_STATE_LABELS: ProjectConfig["stateLabels"] = {
110
+ export const DEFAULT_STATE_LABELS: ProjectConfig["stateLabels"] = {
99
111
  inProgress: "agent:in-progress",
100
112
  blocked: "agent:blocked",
101
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",
102
117
  };
103
118
 
104
119
  /**
@@ -313,6 +328,34 @@ export function resolvePolicy(p: ProjectConfig): ProjectPolicy {
313
328
  return clonePolicy(p.policy ?? DEFAULT_PROJECT_POLICY);
314
329
  }
315
330
 
331
+ /**
332
+ * How this project's `arm` proves a human just approved arming (#613),
333
+ * complete.
334
+ *
335
+ * The loader always materialises `arm` (finalizeProject), so this is only ever
336
+ * the fallback for a `ProjectConfig` that never went through it — a hand-built
337
+ * one in a test, or a config written before the key existed. Absent resolves
338
+ * to {@link DEFAULT_ARM_PROOF} (`challenge`): the authenticated round-trip,
339
+ * which is the behaviour every existing install already has.
340
+ */
341
+ export function resolveArmProof(p: ProjectConfig): ArmProof {
342
+ return p.arm?.proof === "claim-only" ? "claim-only" : DEFAULT_ARM_PROOF;
343
+ }
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
+
316
359
  /**
317
360
  * A policy with no array shared with its source.
318
361
  *
@@ -835,12 +878,21 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
835
878
  return `reporting.availability must be an object`;
836
879
  }
837
880
  if (rel.length === 1 && relStr === "escalation") return `escalation must be an object`;
881
+ if (rel.length === 1 && relStr === "arm") {
882
+ return `arm must be an object with a "proof" of ${ARM_PROOF_LIST}`;
883
+ }
838
884
  if (rel.length === 1 && relStr === "authority") {
839
885
  return `authority must be an object with "merge", "release" and "promotion" of ${AUTHORITY_HOLDER_LIST}`;
840
886
  }
841
887
  if (rel.length === 1 && relStr === "policy") {
842
888
  return `policy must be an object with "merge" and "release" sections`;
843
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
+ }
844
896
  if (rel.length === 2 && relStr === "policy") return `${pathStr} must be an object`;
845
897
  if (rel.length === 1 && relStr === "caps") return `caps must be an object`;
846
898
  if (rel.length === 2 && relStr === "caps" && rel[1] === "planUsage") {
@@ -891,6 +943,7 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
891
943
  if (issue.code === "unrecognized_keys") {
892
944
  const keys = (issue.keys ?? []).join(", ");
893
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"`;
894
947
  if (pathStr === "policy.merge") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_MERGE_KEYS}`;
895
948
  if (pathStr === "policy.release") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_RELEASE_KEYS}`;
896
949
  if (pathStr === "caps.planUsage") {
@@ -940,6 +993,9 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
940
993
 
941
994
  // Regex / custom-message failures carry the finding in issue.message.
942
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
+ }
943
999
  // Empty array (reporting.interruptOn, availability.days) has its own line.
944
1000
  if (parentOf(rel) === "reporting.interruptOn") return `reporting.interruptOn must name at least one category`;
945
1001
  if (pathStr === "reporting.availability.days") return `reporting.availability.days must be a non-empty array of ${WEEKDAY_LIST}`;
@@ -955,6 +1011,9 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
955
1011
  }
956
1012
 
957
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
+ }
958
1017
  if (rel.length === 2 && rel[0] === "caps") return capProblem(rel[1] as string, found());
959
1018
  if (pathStr === "caps.planUsage.maxUsedFraction") {
960
1019
  return `caps.planUsage.maxUsedFraction must be a fraction between 0 and 1 (0.85 holds at 85% of the allowance), found ${found()}`;
@@ -995,6 +1054,9 @@ function capProblem(key: string, found: string): string {
995
1054
  const POLICY_MERGE_KEYS = quoteList(Object.keys(clonePolicy(DEFAULT_PROJECT_POLICY).merge));
996
1055
  const POLICY_RELEASE_KEYS = quoteList(Object.keys(clonePolicy(DEFAULT_PROJECT_POLICY).release));
997
1056
 
1057
+ /** The closed `arm.proof` vocabulary, for the not-an-object wording. */
1058
+ const ARM_PROOF_LIST = quoteList(ARM_PROOFS);
1059
+
998
1060
  // ---------------------------------------------------------------------------
999
1061
  // residue: cross-field coherence, migrations, defaults, path expansion
1000
1062
  // ---------------------------------------------------------------------------
@@ -1062,6 +1124,7 @@ function finalizeProject(
1062
1124
 
1063
1125
  const stateLabels = p["stateLabels"] as Raw | undefined;
1064
1126
  const escalation = finalizeEscalation(p["escalation"] as Raw | undefined);
1127
+ const arm = finalizeArm(p["arm"] as Raw | undefined);
1065
1128
  const authority = finalizeAuthority(p["authority"] as Raw | undefined);
1066
1129
  const releasePolicy = finalizeReleasePolicy(p["releasePolicy"], label, problems);
1067
1130
  const strandedTagRepos =
@@ -1077,6 +1140,7 @@ function finalizeProject(
1077
1140
  );
1078
1141
  }
1079
1142
  const policy = finalizePolicy(p["policy"], label, problems);
1143
+ const review = finalizeReview(p["review"] as Raw | undefined);
1080
1144
  const caps = reconcileCaps(p["caps"], `${label}: caps`, problems, legacyCaps);
1081
1145
  const reporting = finalizeReporting(p["reporting"], label, problems);
1082
1146
  const recoveryMerges = (p["recoveryMerges"] as RecoveryMergeAuthorization[] | undefined)?.map(
@@ -1175,6 +1239,7 @@ function finalizeProject(
1175
1239
  inProgress: pickString(stateLabels?.["inProgress"], DEFAULT_STATE_LABELS.inProgress),
1176
1240
  blocked: pickString(stateLabels?.["blocked"], DEFAULT_STATE_LABELS.blocked),
1177
1241
  failed: pickString(stateLabels?.["failed"], DEFAULT_STATE_LABELS.failed),
1242
+ backlog: pickString(stateLabels?.["backlog"], DEFAULT_STATE_LABELS.backlog),
1178
1243
  },
1179
1244
  routing: { labelPrefix, repos },
1180
1245
  caps,
@@ -1183,6 +1248,8 @@ function finalizeProject(
1183
1248
  ...(modelFallbackThreshold === undefined ? {} : { modelFallbackThreshold }),
1184
1249
  ...(effectiveOmpSettings === undefined ? {} : { ompSettings: effectiveOmpSettings }),
1185
1250
  escalation,
1251
+ arm,
1252
+ review,
1186
1253
  authority,
1187
1254
  releasePolicy,
1188
1255
  policy,
@@ -1194,6 +1261,35 @@ function finalizeProject(
1194
1261
  };
1195
1262
  }
1196
1263
 
1264
+ /**
1265
+ * The project's arming gate (#613), complete. A config written before the key
1266
+ * existed — or one that never answered — materialises as `challenge`, the
1267
+ * authenticated round-trip every existing install already runs, so an upgrade
1268
+ * changes no behaviour. zod has already rejected anything that is not a
1269
+ * `"challenge"` / `"claim-only"` string, so this only fills the absent case.
1270
+ */
1271
+ function finalizeArm(parsed: Raw | undefined): ProjectConfig["arm"] {
1272
+ if (parsed === undefined) return { proof: DEFAULT_ARM_PROOF };
1273
+ return { proof: (parsed["proof"] as ArmProof | undefined) ?? DEFAULT_ARM_PROOF };
1274
+ }
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
+
1197
1293
  function finalizeEscalation(parsed: Raw | undefined): ProjectConfig["escalation"] {
1198
1294
  if (parsed === undefined) {
1199
1295
  return { fallbackToIssueComment: true, orchestrator: "embedded" };