omp-conductor 0.18.0 → 0.18.2

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 (65) hide show
  1. package/README.md +35 -1
  2. package/REFERENCE.md +61 -11
  3. package/agents/to-spec.md +94 -0
  4. package/package.json +2 -1
  5. package/schema/config.schema.json +35 -1
  6. package/src/admission.ts +204 -75
  7. package/src/arm-challenge.ts +250 -57
  8. package/src/ask.ts +268 -7
  9. package/src/board.ts +17 -3
  10. package/src/briefs/orchestrator.md +62 -21
  11. package/src/briefs/to-spec.md +88 -0
  12. package/src/briefs/worker.md +2 -1
  13. package/src/cli.ts +124 -1
  14. package/src/command-help.ts +11 -0
  15. package/src/command-manifest.ts +38 -5
  16. package/src/commands/arm.ts +1 -1
  17. package/src/commands/context.ts +1 -0
  18. package/src/commands/drain.ts +176 -0
  19. package/src/commands/extend.ts +6 -10
  20. package/src/commands/intake.ts +4 -19
  21. package/src/commands/status.ts +5 -1
  22. package/src/commands/watch.ts +51 -16
  23. package/src/commands/worker.ts +9 -10
  24. package/src/config-schema.ts +43 -6
  25. package/src/config.ts +65 -9
  26. package/src/daemon.ts +879 -41
  27. package/src/dashboard/app.js +4 -1
  28. package/src/dashboard/server.ts +5 -2
  29. package/src/decisions.ts +243 -17
  30. package/src/diff-flags.ts +75 -1
  31. package/src/doctor.ts +60 -82
  32. package/src/escalate.ts +31 -14
  33. package/src/failure-class.ts +28 -2
  34. package/src/fleet.ts +239 -240
  35. package/src/gitops.ts +188 -81
  36. package/src/graph-health.ts +35 -1
  37. package/src/graph.ts +66 -1
  38. package/src/harness-loader.ts +59 -0
  39. package/src/host.ts +242 -2
  40. package/src/lifecycle.ts +122 -1
  41. package/src/omp-settings.ts +19 -0
  42. package/src/omp.ts +183 -21
  43. package/src/orchestrator-tick.ts +1591 -32
  44. package/src/orchestrator.ts +12 -0
  45. package/src/privileged.ts +1 -4
  46. package/src/release-policy.ts +503 -9
  47. package/src/session-host.ts +65 -6
  48. package/src/settlement.ts +69 -17
  49. package/src/setup-host.ts +1225 -9
  50. package/src/setup-install.ts +28 -0
  51. package/src/setup-wizard.ts +154 -3
  52. package/src/setup.ts +83 -17
  53. package/src/shell.ts +15 -0
  54. package/src/status-render.ts +216 -12
  55. package/src/store.ts +443 -42
  56. package/src/to-spec.ts +408 -0
  57. package/src/tracker/github.ts +104 -14
  58. package/src/types.ts +405 -19
  59. package/src/upgrade-verify.ts +209 -2
  60. package/src/upgrade.ts +175 -1
  61. package/src/verbs/protocol.ts +39 -0
  62. package/src/verbs/server.ts +765 -56
  63. package/src/verbs/socket.ts +24 -5
  64. package/src/worker.ts +12 -2
  65. package/src/worktree.ts +29 -12
@@ -2,13 +2,13 @@
2
2
  * `worker` — pause/resume/stop one live worker session through the daemon's run-control HTTP endpoints.
3
3
  *
4
4
  * Moved out of cli.ts's switch by the per-verb module split (#462);
5
- * only the case wrapper, the injected `ctx` lookups and the imports
6
- * changed from the original bodies.
5
+ * the daemon discovery resolves through {@link requireDaemonControl} so a
6
+ * live systemd-run daemon stays reachable when its pidfile is missing (#811).
7
7
  */
8
8
 
9
9
  import type { CommandContext } from "./context.ts";
10
10
  import { findProject, loadConfig } from "../config.ts";
11
- import { livingDaemon } from "../lifecycle.ts";
11
+ import { requireDaemonControl } from "../lifecycle.ts";
12
12
 
13
13
  export async function workerCommand(ctx: CommandContext): Promise<void> {
14
14
  const sub = ctx.argv[1];
@@ -28,13 +28,12 @@ if (sub === "stop" && (reason === undefined || reason === "" || reason.length >
28
28
  process.exit(2);
29
29
  }
30
30
  const project = findProject(loadConfig(), ctx.projectFlag);
31
- const daemon = livingDaemon();
32
- if (daemon === undefined) throw new Error("daemon is not running");
33
- if (daemon.project !== undefined && daemon.project !== project.name) {
34
- throw new Error(
35
- `daemon serves project "${daemon.project}", not requested project "${project.name}"`,
36
- );
37
- }
31
+ // The pidfile is authoritative when it names a live process; a missing or
32
+ // stale record falls back to the systemd unit, proved through its own
33
+ // /healthz answer, so a live unit-owned daemon is never reported stopped
34
+ // because the runtime directory was wiped (#811). The refusal chain is
35
+ // shared with `extend` through requireDaemonControl.
36
+ const daemon = await requireDaemonControl(project.name);
38
37
  const response = await fetch(
39
38
  `http://127.0.0.1:${daemon.port}/runs/${issue}/${sub}`,
40
39
  {
@@ -31,6 +31,7 @@ import {
31
31
  DEFAULT_AUTHORITY,
32
32
  DEFAULT_CAPS,
33
33
  DEFAULT_PROJECT_POLICY,
34
+ DEFAULT_REVIEW_ADJUDICATOR_ROLE,
34
35
  DEFAULT_REVIEW_MAX_ROUNDS,
35
36
  DEFAULT_REVIEW_STRICTNESS,
36
37
  DRAFT_POLICIES,
@@ -41,6 +42,7 @@ import {
41
42
  RELEASE_REQUIREMENTS,
42
43
  RELEASE_SHAPES,
43
44
  REPORT_SCOPES,
45
+ REVIEW_ADJUDICATOR_RE,
44
46
  REVIEW_MAX_ROUNDS_MAX,
45
47
  REVIEW_MAX_ROUNDS_MIN,
46
48
  REVIEW_STRICTNESS,
@@ -265,11 +267,15 @@ const armSchema = z
265
267
 
266
268
  /**
267
269
  * 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.
270
+ * and returned, the hard ceiling on review rounds per PR lifecycle, and which
271
+ * OMP model role carries the terminal review-ceiling adjudication (#875).
272
+ * Absent or a legacy config without the keys loads as `medium` with
273
+ * {@link DEFAULT_REVIEW_MAX_ROUNDS} rounds and
274
+ * {@link DEFAULT_REVIEW_ADJUDICATOR_ROLE} as adjudicator the recommended
275
+ * defaults for a new project, materialised deterministically for every
276
+ * existing one, with the schema bounds and the runtime defaults read from the
277
+ * same constants. The adjudicator names a role only; a provider/model is
278
+ * refused here, because OMP owns exact provider selection.
273
279
  */
274
280
  const reviewSchema = z
275
281
  .object({
@@ -280,9 +286,16 @@ const reviewSchema = z
280
286
  .min(REVIEW_MAX_ROUNDS_MIN)
281
287
  .max(REVIEW_MAX_ROUNDS_MAX)
282
288
  .default(DEFAULT_REVIEW_MAX_ROUNDS),
289
+ adjudicator: z
290
+ .string()
291
+ .regex(
292
+ REVIEW_ADJUDICATOR_RE,
293
+ "must name one OMP model role — a single role token like \"task\" (never a provider/model, which omp owns)",
294
+ )
295
+ .default(DEFAULT_REVIEW_ADJUDICATOR_ROLE),
283
296
  })
284
297
  .strict()
285
- .describe("Review strictness and round ceiling for green PRs");
298
+ .describe("Review strictness, round ceiling and adjudicator role for green PRs");
286
299
 
287
300
  const releasePolicySchema = z.union([
288
301
  releasePolicyLegacyEnum,
@@ -336,6 +349,25 @@ const stateLabelsSchema = z
336
349
  })
337
350
  .partial();
338
351
 
352
+ // ---------------------------------------------------------------------------
353
+ // Host constraints (#721)
354
+ // ---------------------------------------------------------------------------
355
+
356
+ const hostConstraintsSchema = z
357
+ .object({
358
+ description: z.string().min(1).describe("What this host is and what else it runs"),
359
+ path: z
360
+ .string()
361
+ .min(1)
362
+ .describe('The non-interactive PATH a script or `ssh host "<cmd>"` invocation must export'),
363
+ conventions: z
364
+ .record(z.string().min(1), z.string().min(1))
365
+ .describe("Per-repo command conventions, keyed by the brief's `owner/repo` slug"),
366
+ })
367
+ .strict()
368
+ .partial()
369
+ .describe("Operator-authored host facts rendered into every worker brief");
370
+
339
371
  // ---------------------------------------------------------------------------
340
372
  // Project / root
341
373
  // ---------------------------------------------------------------------------
@@ -409,6 +441,11 @@ const configSchema = z
409
441
  .min(1, "must name a directory")
410
442
  .optional()
411
443
  .describe("Absolute directory for restorable conductor.db snapshots; defaults to <stateDir>/backups/db"),
444
+ // Cores and RAM are derived by host.ts at render time; only what the
445
+ // operator must type belongs in this block.
446
+ host: hostConstraintsSchema
447
+ .optional()
448
+ .describe("Host facts every worker brief renders; absent renders no section"),
412
449
  projects: z.array(projectSchema).min(1, `"projects" must be a non-empty array — the dispatcher has nothing to service otherwise`),
413
450
  })
414
451
  .loose()
package/src/config.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  DEFAULT_PROJECT_POLICY,
29
29
  DEFAULT_REPORT_POLICY,
30
30
  DEFAULT_REPORT_SCOPE,
31
+ DEFAULT_REVIEW_ADJUDICATOR_ROLE,
31
32
  DEFAULT_REVIEW_MAX_ROUNDS,
32
33
  DEFAULT_REVIEW_POLICY,
33
34
  DEFAULT_REVIEW_STRICTNESS,
@@ -49,6 +50,7 @@ import {
49
50
  type Caps,
50
51
  type ConductorConfig,
51
52
  type DigestCadence,
53
+ type HostConstraints,
52
54
  type InterruptCategory,
53
55
  type MergePreconditions,
54
56
  type PlanUsageCap,
@@ -348,12 +350,20 @@ export function resolveArmProof(p: ProjectConfig): ArmProof {
348
350
  * The loader always materialises `review` (finalizeProject), so this is only
349
351
  * ever the fallback for a `ProjectConfig` that never went through it — a
350
352
  * 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.
353
+ * Absent (or a legacy config that stopped at strictness and maxRounds)
354
+ * resolves to {@link DEFAULT_REVIEW_POLICY}, the documented migration default
355
+ * every existing install deterministically upgrades to the adjudicator role
356
+ * included, so no consumer ever reads a policy without one (#875). A fresh
357
+ * object each call, so mutating the result cannot reach the config it came
358
+ * from.
354
359
  */
355
360
  export function resolveReview(p: ProjectConfig): ReviewPolicy {
356
- return { ...(p.review ?? DEFAULT_REVIEW_POLICY) };
361
+ const base = p.review ?? DEFAULT_REVIEW_POLICY;
362
+ return {
363
+ strictness: base.strictness ?? DEFAULT_REVIEW_STRICTNESS,
364
+ maxRounds: base.maxRounds ?? DEFAULT_REVIEW_MAX_ROUNDS,
365
+ adjudicator: base.adjudicator ?? DEFAULT_REVIEW_ADJUDICATOR_ROLE,
366
+ };
357
367
  }
358
368
 
359
369
  /**
@@ -888,11 +898,14 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
888
898
  return `policy must be an object with "merge" and "release" sections`;
889
899
  }
890
900
  if (rel.length === 1 && relStr === "review") {
891
- return `review must be an object with "strictness" and "maxRounds"`;
901
+ return `review must be an object with "strictness", "maxRounds" and "adjudicator"`;
892
902
  }
893
903
  if (rel.length === 2 && rel[0] === "review" && rel[1] === "maxRounds") {
894
904
  return `review.maxRounds must be an integer between ${REVIEW_MAX_ROUNDS_MIN} and ${REVIEW_MAX_ROUNDS_MAX}, found ${found()}`;
895
905
  }
906
+ if (rel.length === 2 && rel[0] === "review" && rel[1] === "adjudicator") {
907
+ return `review.adjudicator must be a string naming one OMP model role, found ${found()}`;
908
+ }
896
909
  if (rel.length === 2 && relStr === "policy") return `${pathStr} must be an object`;
897
910
  if (rel.length === 1 && relStr === "caps") return `caps must be an object`;
898
911
  if (rel.length === 2 && relStr === "caps" && rel[1] === "planUsage") {
@@ -943,7 +956,7 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
943
956
  if (issue.code === "unrecognized_keys") {
944
957
  const keys = (issue.keys ?? []).join(", ");
945
958
  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"`;
959
+ if (pathStr === "review") return `review has unknown key(s): ${keys} — expected "strictness", "maxRounds" and "adjudicator"`;
947
960
  if (pathStr === "policy.merge") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_MERGE_KEYS}`;
948
961
  if (pathStr === "policy.release") return `${pathStr} has unknown key(s): ${keys} — expected ${POLICY_RELEASE_KEYS}`;
949
962
  if (pathStr === "caps.planUsage") {
@@ -1092,8 +1105,48 @@ function finalize(data: unknown, path: string): ConductorConfig {
1092
1105
  });
1093
1106
  }
1094
1107
 
1108
+ const rawHost = root["host"] as Raw | undefined;
1109
+ const host = finalizeHost(rawHost);
1110
+ const rawDbBackupDir = root["dbBackupDir"];
1111
+ const dbBackupDir = typeof rawDbBackupDir === "string" ? rawDbBackupDir : undefined;
1112
+
1095
1113
  if (problems.length > 0) throw new Error(problemEnvelope(path, problems));
1096
- return { version: CONFIG_VERSION, defaults, projects };
1114
+ // `ConductorConfig` is rebuilt here, so every optional top-level key must be
1115
+ // spelled out or it silently vanishes from every loaded config. `dbBackupDir`
1116
+ // and `host` are the two today; a third needs its own line below (#773).
1117
+ return {
1118
+ version: CONFIG_VERSION,
1119
+ defaults,
1120
+ projects,
1121
+ ...(dbBackupDir === undefined ? {} : { dbBackupDir }),
1122
+ ...(host === undefined ? {} : { host }),
1123
+ };
1124
+ }
1125
+
1126
+ /**
1127
+ * The typed host-constraints block (#721), or `undefined` when nothing usable
1128
+ * was configured — matching the schema's "absent renders no section" promise.
1129
+ * Trimming and empty-drop mirror the other hand-edited string fields; an
1130
+ * object whose every key is empty loads as if the field were absent.
1131
+ */
1132
+ function finalizeHost(parsed: Raw | undefined): HostConstraints | undefined {
1133
+ if (parsed === undefined) return undefined;
1134
+ const out: HostConstraints = {};
1135
+ const description = parsed["description"];
1136
+ if (typeof description === "string" && description.trim() !== "") out.description = description.trim();
1137
+ const path = parsed["path"];
1138
+ if (typeof path === "string" && path.trim() !== "") out.path = path.trim();
1139
+ const conventions = parsed["conventions"];
1140
+ if (typeof conventions === "object" && conventions !== null && !Array.isArray(conventions)) {
1141
+ const kept: Record<string, string> = {};
1142
+ for (const [slug, value] of Object.entries(conventions as Record<string, unknown>)) {
1143
+ if (typeof value === "string" && value.trim() !== "" && slug.trim() !== "") {
1144
+ kept[slug.trim()] = value.trim();
1145
+ }
1146
+ }
1147
+ if (Object.keys(kept).length > 0) out.conventions = kept;
1148
+ }
1149
+ return Object.keys(out).length === 0 ? undefined : out;
1097
1150
  }
1098
1151
 
1099
1152
  function finalizeProject(
@@ -1279,14 +1332,17 @@ function finalizeArm(parsed: Raw | undefined): ProjectConfig["arm"] {
1279
1332
  * {@link DEFAULT_REVIEW_POLICY}, the documented migration default, so an
1280
1333
  * upgrade changes no existing project's behaviour beyond what its operators
1281
1334
  * 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.
1335
+ * strictness vocabulary, the validated round range or the adjudicator role
1336
+ * token grammar, so this only fills the absent-case defaults — the
1337
+ * adjudicator included (#875), deterministically the same role a new
1338
+ * project's setup starts from.
1284
1339
  */
1285
1340
  function finalizeReview(parsed: Raw | undefined): ProjectConfig["review"] {
1286
1341
  if (parsed === undefined) return { ...DEFAULT_REVIEW_POLICY };
1287
1342
  return {
1288
1343
  strictness: (parsed["strictness"] as ReviewStrictness | undefined) ?? DEFAULT_REVIEW_STRICTNESS,
1289
1344
  maxRounds: (parsed["maxRounds"] as number | undefined) ?? DEFAULT_REVIEW_MAX_ROUNDS,
1345
+ adjudicator: (parsed["adjudicator"] as string | undefined) ?? DEFAULT_REVIEW_ADJUDICATOR_ROLE,
1290
1346
  };
1291
1347
  }
1292
1348