omp-conductor 0.16.2 → 0.17.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.
Files changed (52) hide show
  1. package/README.md +38 -4
  2. package/REFERENCE.md +18 -12
  3. package/package.json +2 -1
  4. package/schema/config.schema.json +16 -0
  5. package/src/admission.ts +159 -43
  6. package/src/availability.ts +27 -1
  7. package/src/briefs/worker.md +2 -0
  8. package/src/clack-ui.ts +83 -0
  9. package/src/command-manifest.ts +16 -7
  10. package/src/commands/arm.ts +11 -3
  11. package/src/commands/decision.ts +17 -7
  12. package/src/commands/doctor.ts +18 -1
  13. package/src/commands/hold.ts +9 -7
  14. package/src/commands/ledger.ts +25 -4
  15. package/src/commands/message.ts +32 -4
  16. package/src/commands/setup.ts +61 -10
  17. package/src/commands/stats.ts +9 -5
  18. package/src/commands/status.ts +32 -5
  19. package/src/commands/tail.ts +13 -1
  20. package/src/commands/watch.ts +16 -7
  21. package/src/config-schema.ts +20 -0
  22. package/src/config.ts +37 -0
  23. package/src/daemon.ts +1240 -18
  24. package/src/doctor.ts +310 -22
  25. package/src/escalate.ts +560 -57
  26. package/src/failure-class.ts +56 -13
  27. package/src/fleet.ts +224 -47
  28. package/src/gitops.ts +103 -24
  29. package/src/lifecycle.ts +7 -2
  30. package/src/orchestrator-tick.ts +372 -157
  31. package/src/privileged.ts +3 -0
  32. package/src/release-policy.ts +177 -5
  33. package/src/setup-answers.ts +135 -0
  34. package/src/setup-host.ts +193 -4
  35. package/src/setup-install.ts +2 -0
  36. package/src/setup-probe.ts +1 -0
  37. package/src/setup-wizard.ts +1296 -101
  38. package/src/setup.ts +60 -3
  39. package/src/status-render.ts +11 -1
  40. package/src/store.ts +333 -12
  41. package/src/tracker/github.ts +562 -13
  42. package/src/types.ts +204 -2
  43. package/src/ui/progress.ts +32 -0
  44. package/src/ui/style.ts +11 -0
  45. package/src/upgrade.ts +50 -19
  46. package/src/verbs/actions.ts +66 -18
  47. package/src/verbs/protocol.ts +45 -0
  48. package/src/verbs/server.ts +212 -11
  49. package/src/wizard-ui.ts +14 -5
  50. package/src/worker.ts +26 -0
  51. package/systemd/omp-conductor-recover.sh +73 -0
  52. package/systemd/recover-unit-test.sh +61 -0
@@ -50,18 +50,27 @@ export async function watchCommand(ctx: CommandContext): Promise<void> {
50
50
  }
51
51
 
52
52
  if (sub === "list" || sub === undefined) {
53
- const watches = store.openDecisions(project.name).filter((d) => d.kind === "watch");
53
+ const open = store.openDecisions(project.name).filter((d) => d.kind === "watch");
54
+ const now = Date.now();
55
+ const watches = open.map((d) => ({
56
+ id: d.id,
57
+ ageHours: Math.max(0, Math.round((now - d.askedAt) / 3_600_000)),
58
+ blocks: d.blocks ?? null,
59
+ condition: d.condition === undefined ? null : d.conditionMetAt === undefined ? "pending" : "met",
60
+ note: d.question,
61
+ }));
62
+ if (ctx.argv.includes("--json")) {
63
+ process.stdout.write(`${JSON.stringify({ project: project.name, watches }, null, 2)}\n`);
64
+ return;
65
+ }
54
66
  if (watches.length === 0) {
55
67
  process.stdout.write("no watches\n");
56
68
  return;
57
69
  }
58
- const now = Date.now();
59
- for (const d of watches) {
60
- const condition =
61
- d.condition === undefined ? "-" : d.conditionMetAt === undefined ? "pending" : "met";
62
- const hours = Math.max(0, Math.round((now - d.askedAt) / 3_600_000));
70
+ for (const watch of watches) {
63
71
  process.stdout.write(
64
- `${d.id} ${hours}h blocks:${d.blocks ?? "-"} condition:${condition} ${d.question}\n`,
72
+ `${watch.id} ${watch.ageHours}h blocks:${watch.blocks ?? "-"} ` +
73
+ `condition:${watch.condition ?? "-"} ${watch.note}\n`,
65
74
  );
66
75
  }
67
76
  return;
@@ -22,10 +22,12 @@
22
22
 
23
23
  import { z } from "zod";
24
24
  import {
25
+ ARM_PROOFS,
25
26
  AUTHORITY_HOLDERS,
26
27
  BASE_FRESHNESS,
27
28
  BEHIND_BASE_ACTIONS,
28
29
  CONFIG_VERSION,
30
+ DEFAULT_ARM_PROOF,
29
31
  DEFAULT_AUTHORITY,
30
32
  DEFAULT_CAPS,
31
33
  DEFAULT_PROJECT_POLICY,
@@ -239,6 +241,21 @@ const escalationSchema = z
239
241
  })
240
242
  .strict();
241
243
 
244
+ /**
245
+ * The per-project arming gate (conductor #613): how `arm` proves a human just
246
+ * approved dispatch. Absent or a legacy config without the key loads as
247
+ * `challenge` — the authenticated round-trip, which is what existing installs
248
+ * already run. `claim-only` arms on the shared live-plumbing verdict with no
249
+ * Telegram send or wait, so an unattended recovery can re-arm a project that
250
+ * opted in.
251
+ */
252
+ const armSchema = z
253
+ .object({
254
+ proof: z.enum([...ARM_PROOFS]).default(DEFAULT_ARM_PROOF),
255
+ })
256
+ .strict()
257
+ .describe("How `arm` proves a human just approved arming");
258
+
242
259
  const releasePolicySchema = z.union([
243
260
  releasePolicyLegacyEnum,
244
261
  z.record(z.string(), authorityHolderEnum),
@@ -330,6 +347,9 @@ const projectSchema = z
330
347
  // overlay); anything else is dropped by the loader, like `workerModel`.
331
348
  workerAdvisor: z.unknown().optional(),
332
349
  escalation: escalationSchema.optional(),
350
+ // How `arm` proves a human approved arming (#613); absent loads as
351
+ // `challenge`, preserving today's authenticated round-trip.
352
+ arm: armSchema.optional(),
333
353
  authority: authoritySchema.optional(),
334
354
  releasePolicy: releasePolicySchema.optional(),
335
355
  policy: projectPolicySchema.optional(),
package/src/config.ts CHANGED
@@ -17,10 +17,12 @@ 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,
@@ -55,6 +57,7 @@ import {
55
57
  type WeeklyAvailability,
56
58
  type RepoTarget,
57
59
  type ResolvedGrants,
60
+ type ArmProof,
58
61
  } from "./types.ts";
59
62
  import {
60
63
  AUTHORITY_HOLDER_LIST,
@@ -313,6 +316,20 @@ export function resolvePolicy(p: ProjectConfig): ProjectPolicy {
313
316
  return clonePolicy(p.policy ?? DEFAULT_PROJECT_POLICY);
314
317
  }
315
318
 
319
+ /**
320
+ * How this project's `arm` proves a human just approved arming (#613),
321
+ * complete.
322
+ *
323
+ * The loader always materialises `arm` (finalizeProject), so this is only ever
324
+ * the fallback for a `ProjectConfig` that never went through it — a hand-built
325
+ * one in a test, or a config written before the key existed. Absent resolves
326
+ * to {@link DEFAULT_ARM_PROOF} (`challenge`): the authenticated round-trip,
327
+ * which is the behaviour every existing install already has.
328
+ */
329
+ export function resolveArmProof(p: ProjectConfig): ArmProof {
330
+ return p.arm?.proof === "claim-only" ? "claim-only" : DEFAULT_ARM_PROOF;
331
+ }
332
+
316
333
  /**
317
334
  * A policy with no array shared with its source.
318
335
  *
@@ -835,6 +852,9 @@ function clauseFor(rel: readonly PropertyKey[], issue: {
835
852
  return `reporting.availability must be an object`;
836
853
  }
837
854
  if (rel.length === 1 && relStr === "escalation") return `escalation must be an object`;
855
+ if (rel.length === 1 && relStr === "arm") {
856
+ return `arm must be an object with a "proof" of ${ARM_PROOF_LIST}`;
857
+ }
838
858
  if (rel.length === 1 && relStr === "authority") {
839
859
  return `authority must be an object with "merge", "release" and "promotion" of ${AUTHORITY_HOLDER_LIST}`;
840
860
  }
@@ -995,6 +1015,9 @@ function capProblem(key: string, found: string): string {
995
1015
  const POLICY_MERGE_KEYS = quoteList(Object.keys(clonePolicy(DEFAULT_PROJECT_POLICY).merge));
996
1016
  const POLICY_RELEASE_KEYS = quoteList(Object.keys(clonePolicy(DEFAULT_PROJECT_POLICY).release));
997
1017
 
1018
+ /** The closed `arm.proof` vocabulary, for the not-an-object wording. */
1019
+ const ARM_PROOF_LIST = quoteList(ARM_PROOFS);
1020
+
998
1021
  // ---------------------------------------------------------------------------
999
1022
  // residue: cross-field coherence, migrations, defaults, path expansion
1000
1023
  // ---------------------------------------------------------------------------
@@ -1062,6 +1085,7 @@ function finalizeProject(
1062
1085
 
1063
1086
  const stateLabels = p["stateLabels"] as Raw | undefined;
1064
1087
  const escalation = finalizeEscalation(p["escalation"] as Raw | undefined);
1088
+ const arm = finalizeArm(p["arm"] as Raw | undefined);
1065
1089
  const authority = finalizeAuthority(p["authority"] as Raw | undefined);
1066
1090
  const releasePolicy = finalizeReleasePolicy(p["releasePolicy"], label, problems);
1067
1091
  const strandedTagRepos =
@@ -1183,6 +1207,7 @@ function finalizeProject(
1183
1207
  ...(modelFallbackThreshold === undefined ? {} : { modelFallbackThreshold }),
1184
1208
  ...(effectiveOmpSettings === undefined ? {} : { ompSettings: effectiveOmpSettings }),
1185
1209
  escalation,
1210
+ arm,
1186
1211
  authority,
1187
1212
  releasePolicy,
1188
1213
  policy,
@@ -1194,6 +1219,18 @@ function finalizeProject(
1194
1219
  };
1195
1220
  }
1196
1221
 
1222
+ /**
1223
+ * The project's arming gate (#613), complete. A config written before the key
1224
+ * existed — or one that never answered — materialises as `challenge`, the
1225
+ * authenticated round-trip every existing install already runs, so an upgrade
1226
+ * changes no behaviour. zod has already rejected anything that is not a
1227
+ * `"challenge"` / `"claim-only"` string, so this only fills the absent case.
1228
+ */
1229
+ function finalizeArm(parsed: Raw | undefined): ProjectConfig["arm"] {
1230
+ if (parsed === undefined) return { proof: DEFAULT_ARM_PROOF };
1231
+ return { proof: (parsed["proof"] as ArmProof | undefined) ?? DEFAULT_ARM_PROOF };
1232
+ }
1233
+
1197
1234
  function finalizeEscalation(parsed: Raw | undefined): ProjectConfig["escalation"] {
1198
1235
  if (parsed === undefined) {
1199
1236
  return { fallbackToIssueComment: true, orchestrator: "embedded" };