omp-conductor 0.15.13 → 0.16.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 (47) hide show
  1. package/REFERENCE.md +72 -2
  2. package/package.json +2 -1
  3. package/schema/config.schema.json +6 -0
  4. package/src/admission.ts +745 -0
  5. package/src/ask.ts +47 -0
  6. package/src/backups.ts +19 -7
  7. package/src/board.ts +1 -2
  8. package/src/briefs/orchestrator.md +62 -4
  9. package/src/cli.ts +26 -0
  10. package/src/commands/context.ts +3 -0
  11. package/src/commands/decision.ts +10 -1
  12. package/src/commands/doctor.ts +2 -0
  13. package/src/commands/message.ts +8 -1
  14. package/src/commands/restart.ts +15 -3
  15. package/src/commands/restore-db.ts +146 -0
  16. package/src/commands/stop.ts +24 -15
  17. package/src/commands/unfreeze.ts +56 -0
  18. package/src/commands/watch.ts +77 -0
  19. package/src/config-schema.ts +9 -0
  20. package/src/config.ts +24 -0
  21. package/src/daemon.ts +239 -530
  22. package/src/dashboard/server.ts +2 -1
  23. package/src/decisions.ts +32 -7
  24. package/src/depends-on.ts +73 -0
  25. package/src/doctor.ts +178 -5
  26. package/src/escalate.ts +114 -15
  27. package/src/failure-class.ts +47 -0
  28. package/src/fleet.ts +41 -410
  29. package/src/gitops.ts +86 -1
  30. package/src/log.ts +40 -0
  31. package/src/model-fallback.ts +3 -2
  32. package/src/omp-settings.ts +114 -0
  33. package/src/omp.ts +39 -0
  34. package/src/orchestrator-tick.ts +7 -1
  35. package/src/reports.ts +124 -12
  36. package/src/session-host.ts +6 -0
  37. package/src/setup-wizard.ts +36 -0
  38. package/src/setup.ts +58 -1
  39. package/src/status-render.ts +445 -0
  40. package/src/stop-provenance.ts +53 -0
  41. package/src/store.ts +352 -11
  42. package/src/types.ts +187 -4
  43. package/src/unblock.ts +1 -1
  44. package/src/upgrade-verify.ts +1 -1
  45. package/src/upgrade.ts +1 -2
  46. package/src/verbs/server.ts +25 -0
  47. package/src/worker.ts +162 -10
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `unfreeze <repo>` — the operator's sanctioned override for a base-red-freeze.
3
+ *
4
+ * A base-red-freeze is a mechanical gate: `prMergeVerb` refuses every
5
+ * `conductor_pr_merge` to a repo whose base is red, and the floor binds even
6
+ * the orchestrator to that refusal. This verb is the *one* sanctioned way to
7
+ * lift it early — after the operator has judged the base repaired (or the
8
+ * merge warranted anyway) — and it is ledger-recorded: the freeze row keeps
9
+ * `clearedBy`/`clearedReason`/`clearedAt` as its durable override audit, and a
10
+ * material event names the override for the queue digest. It is deliberately
11
+ * not label surgery or a raw DB edit, and it does not re-arm — a still-red base
12
+ * will re-freeze the repo on the next watch observation.
13
+ */
14
+
15
+ import type { CommandContext } from "./context.ts";
16
+ import { findProject, loadConfig } from "../config.ts";
17
+ import { dbPath, openStore } from "../store.ts";
18
+
19
+ export async function unfreezeCommand(ctx: CommandContext): Promise<void> {
20
+ const repo = ctx.argv[1];
21
+ if (repo === undefined || repo.length === 0 || repo.startsWith("-")) {
22
+ process.stderr.write("omp-conductor: unfreeze needs the routed repo name, e.g. `omp-conductor unfreeze api`\n");
23
+ process.exit(2);
24
+ }
25
+ const reason = ctx.flag("--reason") ?? "operator-override";
26
+ const cfg = loadConfig();
27
+ const project = findProject(cfg, ctx.projectFlag);
28
+ const store = openStore(dbPath());
29
+ try {
30
+ const now = Date.now();
31
+ const lifted = store.clearBaseFreeze(project.name, repo, "operator", reason, now);
32
+ if (!lifted) {
33
+ process.stdout.write(
34
+ `unfreeze: ${repo} has no active base-red freeze in ${project.name} (merges already allowed). Nothing to lift.\n`,
35
+ );
36
+ return;
37
+ }
38
+ store.recordMaterialEvent({
39
+ project: project.name,
40
+ category: "unfreeze",
41
+ summary: `operator unfroze merges to ${repo} (${reason})`,
42
+ evidence:
43
+ `An operator lifted the base-red-freeze on ${repo} with reason "${reason}". ` +
44
+ "This is the sanctioned override path — the freeze re-arms automatically if the base is " +
45
+ "observed red again.",
46
+ occurredAt: now,
47
+ recordedAt: now,
48
+ });
49
+ process.stdout.write(
50
+ `unfreeze: ${repo} base-red freeze lifted in ${project.name} (reason: ${reason}). ` +
51
+ "Merges to this repo resume; a still-red base will re-freeze on the next watch observation.\n",
52
+ );
53
+ } finally {
54
+ store.close();
55
+ }
56
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `watch` — set a condition for the orchestrator itself, with no human in the
3
+ * loop (#459).
4
+ *
5
+ * The split exists because the old `decision open --resolves-when` kept
6
+ * producing rows rendered as questions put to the operator — the fleet looked
7
+ * like it was waiting on a person when it was waiting on GitHub. A watch is a
8
+ * row the orchestrator opened for itself: either a condition the daemon checks
9
+ * (`--resolves-when`) that wakes the next tick when met, or a carry note the
10
+ * next tick should read, neither of which ever needs an operator answer. It is
11
+ * distinguished durably by its `kind`, never by whether it carries a
12
+ * condition — a real question may carry one too.
13
+ */
14
+
15
+ import type { CommandContext } from "./context.ts";
16
+ import { findProject, loadConfig } from "../config.ts";
17
+ import { CONDITION_FORMS, parseCondition } from "../decisions.ts";
18
+ import { dbPath, openStore } from "../store.ts";
19
+
20
+ export async function watchCommand(ctx: CommandContext): Promise<void> {
21
+ const sub = ctx.argv[1];
22
+ const project = findProject(loadConfig(), ctx.projectFlag);
23
+ const store = openStore(dbPath());
24
+ try {
25
+ if (sub === "add") {
26
+ const note = ctx.flag("note")?.trim();
27
+ if (note === undefined || note.length === 0 || note.startsWith("--")) {
28
+ process.stderr.write("omp-conductor: watch add needs --note with what the next tick should know\n");
29
+ process.exit(2);
30
+ }
31
+ const condition = ctx.flag("resolves-when")?.trim();
32
+ if (condition !== undefined && parseCondition(condition) === undefined) {
33
+ process.stderr.write(
34
+ `omp-conductor: --resolves-when must be one of:\n${CONDITION_FORMS.map((f) => ` ${f}`).join("\n")}\n`,
35
+ );
36
+ process.exit(2);
37
+ }
38
+ const blocks = ctx.flag("blocks")?.trim();
39
+ const watch = store.createDecision({
40
+ project: project.name,
41
+ question: note,
42
+ kind: "watch",
43
+ ...(blocks === undefined || blocks.length === 0 ? {} : { blocks }),
44
+ ...(condition === undefined ? {} : { condition }),
45
+ at: Date.now(),
46
+ });
47
+ const wake = condition === undefined ? "read by the next tick" : "the daemon wakes the next tick when it is met";
48
+ process.stdout.write(`watch ${watch.id} added — ${wake} (no operator answer needed)\n`);
49
+ return;
50
+ }
51
+
52
+ if (sub === "list" || sub === undefined) {
53
+ const watches = store.openDecisions(project.name).filter((d) => d.kind === "watch");
54
+ if (watches.length === 0) {
55
+ process.stdout.write("no watches\n");
56
+ return;
57
+ }
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));
63
+ process.stdout.write(
64
+ `${d.id} ${hours}h blocks:${d.blocks ?? "-"} condition:${condition} ${d.question}\n`,
65
+ );
66
+ }
67
+ return;
68
+ }
69
+
70
+ process.stderr.write(
71
+ `omp-conductor: unknown watch subcommand "${sub}" — expected add or list\n`,
72
+ );
73
+ process.exit(2);
74
+ } finally {
75
+ store.close();
76
+ }
77
+ }
@@ -321,6 +321,10 @@ const projectSchema = z
321
321
  // usable, exactly like `workerModel`.
322
322
  modelFallbacks: z.unknown().optional(),
323
323
  modelFallbackThreshold: z.unknown().optional(),
324
+ // The fleet-owned omp settings overlay (#537): an opaque map omp's own
325
+ // schema owns. Conductor validates YAML shape only — the loader keeps it
326
+ // when it is a mapping and drops anything else, like `modelFallbacks`.
327
+ ompSettings: z.unknown().optional(),
324
328
  escalation: escalationSchema.optional(),
325
329
  authority: authoritySchema.optional(),
326
330
  releasePolicy: releasePolicySchema.optional(),
@@ -344,6 +348,11 @@ const configSchema = z
344
348
  $schema: z.string().optional().describe("Path to the shipped config.schema.json"),
345
349
  version: z.number().describe(`The config format version (${READABLE_CONFIG_VERSIONS.join(" or ")})`),
346
350
  defaults: capsSchema.optional(),
351
+ dbBackupDir: z
352
+ .string()
353
+ .min(1, "must name a directory")
354
+ .optional()
355
+ .describe("Absolute directory for restorable conductor.db snapshots; defaults to <stateDir>/backups/db"),
347
356
  projects: z.array(projectSchema).min(1, `"projects" must be a non-empty array — the dispatcher has nothing to service otherwise`),
348
357
  })
349
358
  .loose()
package/src/config.ts CHANGED
@@ -124,6 +124,18 @@ export function configBackupDir(): string {
124
124
  return join(stateDir(), "backups", "config");
125
125
  }
126
126
 
127
+ /**
128
+ * Where restorable `conductor.db` snapshots land: the configured `dbBackupDir`,
129
+ * or the state-root default `<stateDir()>/backups/db` beside the config
130
+ * backups when the field is absent. A `~/`-prefixed configured value is
131
+ * expanded like every other hand-written path field.
132
+ */
133
+ export function dbBackupDirFor(cfg: Pick<ConductorConfig, "dbBackupDir"> | undefined): string {
134
+ const configured = cfg?.dbBackupDir;
135
+ const dir = configured !== undefined && configured.trim() !== "" ? expandHome(configured) : join(stateDir(), "backups", "db");
136
+ return dir;
137
+ }
138
+
127
139
  /**
128
140
  * Where per-run worktrees and bare mirrors live when a project names neither.
129
141
  *
@@ -1099,6 +1111,17 @@ function finalizeProject(
1099
1111
  ? rawThreshold
1100
1112
  : undefined;
1101
1113
 
1114
+ // The fleet-owned omp settings overlay (#537): an opaque map omp's own schema
1115
+ // owns, so the loader validates YAML shape only — a non-mapping is dropped
1116
+ // like an unusable `modelFallbacks` entry rather than failing the load, and
1117
+ // an absent field keeps today's dispatch byte for byte. Everything inside the
1118
+ // map is omp's to interpret, never conductor's.
1119
+ const rawOmpSettings = p["ompSettings"];
1120
+ const ompSettings =
1121
+ typeof rawOmpSettings === "object" && rawOmpSettings !== null && !Array.isArray(rawOmpSettings)
1122
+ ? (rawOmpSettings as Record<string, unknown>)
1123
+ : undefined;
1124
+
1102
1125
  // Marked critical-base/safety markers (commit SHAs or refs). A continuation
1103
1126
  // branch must contain every one before the dispatcher may reattach it;
1104
1127
  // unusable entries are dropped like `modelFallbacks` and an absent or empty
@@ -1128,6 +1151,7 @@ function finalizeProject(
1128
1151
  ...(workerModel === undefined ? {} : { workerModel }),
1129
1152
  ...(modelFallbacks.length === 0 ? {} : { modelFallbacks }),
1130
1153
  ...(modelFallbackThreshold === undefined ? {} : { modelFallbackThreshold }),
1154
+ ...(ompSettings === undefined ? {} : { ompSettings }),
1131
1155
  escalation,
1132
1156
  authority,
1133
1157
  releasePolicy,