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.
- package/REFERENCE.md +12 -8
- package/package.json +1 -1
- package/schema/config.schema.json +40 -1
- package/src/admission.ts +263 -44
- package/src/ask.ts +39 -3
- package/src/availability.ts +27 -1
- package/src/backups.ts +2 -2
- package/src/briefs/orchestrator.md +1 -0
- package/src/briefs/worker.md +38 -19
- package/src/command-help.ts +8 -1
- package/src/command-manifest.ts +5 -2
- package/src/commands/arm.ts +6 -3
- package/src/commands/message.ts +32 -4
- package/src/commands/watch.ts +62 -3
- package/src/config-schema.ts +53 -0
- package/src/config.ts +97 -1
- package/src/daemon.ts +1479 -1483
- package/src/decisions.ts +51 -6
- package/src/depends-on.ts +261 -1
- package/src/diff-flags.ts +350 -0
- package/src/digest-schedule.ts +37 -0
- package/src/doctor.ts +310 -22
- package/src/escalate.ts +560 -57
- package/src/failure-class.ts +71 -15
- package/src/fleet.ts +189 -34
- package/src/gitops.ts +103 -24
- package/src/graph-health.ts +20 -7
- package/src/graph.ts +313 -68
- package/src/lifecycle.ts +43 -7
- package/src/omp.ts +42 -0
- package/src/orchestrator-tick.ts +430 -162
- package/src/release-policy.ts +177 -5
- package/src/routing.ts +11 -3
- package/src/session-host.ts +16 -0
- package/src/settlement.ts +1728 -0
- package/src/setup-host.ts +193 -4
- package/src/setup-install.ts +91 -30
- package/src/setup-wizard.ts +1257 -78
- package/src/setup.ts +153 -6
- package/src/status-render.ts +36 -4
- package/src/store.ts +411 -17
- package/src/tracker/github.ts +607 -12
- package/src/types.ts +331 -5
- package/src/upgrade.ts +50 -19
- package/src/verbs/actions.ts +66 -18
- package/src/verbs/protocol.ts +45 -0
- package/src/verbs/server.ts +270 -13
- package/src/worker.ts +239 -6
- package/src/worktree.ts +115 -8
- package/systemd/omp-conductor-recover.sh +73 -0
- 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" };
|