omp-conductor 0.13.0 → 0.15.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 (44) hide show
  1. package/README.md +549 -234
  2. package/package.json +8 -5
  3. package/schema/config.schema.json +609 -0
  4. package/src/availability.ts +165 -0
  5. package/src/board.ts +19 -32
  6. package/src/brief-upgrade.ts +1 -1
  7. package/src/briefs/orchestrator.md +72 -31
  8. package/src/briefs/policy.md +48 -36
  9. package/src/briefs/probes/gates.md +51 -0
  10. package/src/briefs/probes/project-context.md +59 -0
  11. package/src/briefs/probes/release-procedure.md +81 -0
  12. package/src/cli.ts +356 -212
  13. package/src/config-schema.ts +352 -0
  14. package/src/config.ts +1037 -679
  15. package/src/confinement.ts +54 -0
  16. package/src/daemon.ts +644 -390
  17. package/src/diff-flags.ts +73 -4
  18. package/src/digest-schedule.ts +92 -24
  19. package/src/escalate.ts +89 -22
  20. package/src/fleet.ts +351 -46
  21. package/src/generate-schema.ts +21 -0
  22. package/src/graph.ts +3 -3
  23. package/src/host.ts +16 -0
  24. package/src/omp.ts +21 -1
  25. package/src/orchestrator-tick.ts +732 -56
  26. package/src/privileged.ts +264 -0
  27. package/src/reports.ts +203 -6
  28. package/src/session-host.ts +3 -0
  29. package/src/setup-host.ts +209 -24
  30. package/src/setup-install.ts +320 -0
  31. package/src/setup-probe.ts +412 -0
  32. package/src/setup-wizard.ts +1946 -0
  33. package/src/setup.ts +457 -53
  34. package/src/store.ts +610 -98
  35. package/src/tracker/github.ts +43 -5
  36. package/src/types.ts +153 -14
  37. package/src/upgrade.ts +44 -10
  38. package/src/verbs/actions.ts +131 -13
  39. package/src/verbs/server.ts +40 -18
  40. package/src/wizard-ui.ts +249 -0
  41. package/src/worker.ts +24 -7
  42. package/skills/conductor-onboarding/SKILL.md +0 -748
  43. package/skills/conductor-update/SKILL.md +0 -51
  44. package/src/plugin.ts +0 -1495
@@ -0,0 +1,352 @@
1
+ /**
2
+ * The zod schema for the on-disk conductor config, and the JSON Schema it
3
+ * renders.
4
+ *
5
+ * `ConductorConfig`'s vocabulary was validated field by field by hand-written
6
+ * guards in `config.ts`. This file is the upgrade those guards always pointed
7
+ * at (see the note on `Raw` in `config.ts`): the shape, the value types and
8
+ * every closed enum are now a declarative schema built from the same exported
9
+ * `as const` arrays `types.ts` publishes, so the on-disk vocabulary, the load
10
+ * gate and the shipped `config.schema.json` cannot fork.
11
+ *
12
+ * This schema is the per-field authority: types, enums, numeric bounds,
13
+ * required/optional, and defaults all live here. What it deliberately does
14
+ * **not** express — because the loader must, and zod cannot cleanly — is left
15
+ * to the residue normalisers in `config.ts`: cross-field coherence
16
+ * (scope-vs-explicit reporting, digest/availability times, `start !== end`),
17
+ * the version-keyed cap migration, the clone-URL credential rejection, and
18
+ * `~`/default path expansion. Editing a schema node here changes both what
19
+ * `loadConfig` accepts and the shipped schema; `bun test`'s freshness lock
20
+ * fails if the checked-in JSON is not regenerated.
21
+ */
22
+
23
+ import { z } from "zod";
24
+ import {
25
+ AUTHORITY_HOLDERS,
26
+ BASE_FRESHNESS,
27
+ BEHIND_BASE_ACTIONS,
28
+ CONFIG_VERSION,
29
+ DEFAULT_AUTHORITY,
30
+ DEFAULT_CAPS,
31
+ DEFAULT_PROJECT_POLICY,
32
+ DRAFT_POLICIES,
33
+ INTERRUPT_CATEGORIES,
34
+ LEGACY_RELEASE_POLICIES,
35
+ ORCHESTRATOR_MODES,
36
+ READABLE_CONFIG_VERSIONS,
37
+ RELEASE_REQUIREMENTS,
38
+ RELEASE_SHAPES,
39
+ REPORT_SCOPES,
40
+ WEEKDAYS,
41
+ DIGEST_CADENCES,
42
+ } from "./types.ts";
43
+
44
+ /**
45
+ * The quoted-literal spelling the loader uses in error text (`"a" or "b"`),
46
+ * so a message built from these vocabularies cannot fork from the values it
47
+ * names. Kept here (not in `config.ts`) because `config.ts` imports this file
48
+ * at the parse boundary.
49
+ */
50
+ export function quoteList(values: readonly string[]): string {
51
+ return values.map((v) => `"${v}"`).join(" or ");
52
+ }
53
+
54
+ const REPORT_SCOPE_LIST = quoteList(REPORT_SCOPES);
55
+ const INTERRUPT_CATEGORY_LIST = quoteList(INTERRUPT_CATEGORIES);
56
+ const WEEKDAY_LIST = quoteList(WEEKDAYS);
57
+ const DIGEST_CADENCE_LIST = quoteList(DIGEST_CADENCES);
58
+ const AUTHORITY_HOLDER_LIST = quoteList(AUTHORITY_HOLDERS);
59
+ const BASE_FRESHNESS_LIST = quoteList(BASE_FRESHNESS);
60
+ const DRAFT_POLICY_LIST = quoteList(DRAFT_POLICIES);
61
+ const BEHIND_BASE_ACTION_LIST = quoteList(BEHIND_BASE_ACTIONS);
62
+ const RELEASE_REQUIREMENT_LIST = quoteList(RELEASE_REQUIREMENTS);
63
+ const RELEASE_SHAPE_LIST = quoteList(RELEASE_SHAPES);
64
+ const ORCHESTRATOR_MODE_LIST = quoteList(ORCHESTRATOR_MODES);
65
+ const LEGACY_RELEASE_POLICY_LIST = quoteList(LEGACY_RELEASE_POLICIES);
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Closed vocabularies — every one built from the exported `as const` array in
69
+ // types.ts so a new member is a schema change too, never a silent fork.
70
+ // ---------------------------------------------------------------------------
71
+
72
+ const reportScopeEnum = z.enum([...REPORT_SCOPES]);
73
+ const interruptCategoryEnum = z.enum([...INTERRUPT_CATEGORIES]);
74
+ const weekdayEnum = z.enum([...WEEKDAYS]);
75
+ const digestCadenceEnum = z.enum([...DIGEST_CADENCES]);
76
+ const authorityHolderEnum = z.enum([...AUTHORITY_HOLDERS]);
77
+ const baseFreshnessEnum = z.enum([...BASE_FRESHNESS]);
78
+ const draftPolicyEnum = z.enum([...DRAFT_POLICIES]);
79
+ const behindBaseActionEnum = z.enum([...BEHIND_BASE_ACTIONS]);
80
+ const releaseRequirementEnum = z.enum([...RELEASE_REQUIREMENTS]);
81
+ const releaseShapeEnum = z.enum([...RELEASE_SHAPES]);
82
+ const orchestratorModeEnum = z.enum([...ORCHESTRATOR_MODES]);
83
+ const releasePolicyLegacyEnum = z.enum([...LEGACY_RELEASE_POLICIES]);
84
+
85
+ /** The 24-hour `HH:MM` shape `digest.at` / `availability.start/end` take. */
86
+ const HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Caps
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const planUsageCap = z
93
+ .object({
94
+ windowId: z
95
+ .string()
96
+ .min(1, "must be a non-empty allowance id such as \"anthropic:7d\"")
97
+ .describe("The allowance window this threshold guards"),
98
+ maxUsedFraction: z
99
+ .number()
100
+ .min(0)
101
+ .max(1)
102
+
103
+ .describe("Consumed share, 0–1, at which new claims stop"),
104
+ })
105
+ .strict()
106
+ .describe("An allowance-window spend guard, or null for unmetered");
107
+
108
+ const capsSchema = z
109
+ .object({
110
+ maxConcurrentWorkers: z.number().min(0).describe(`Parallel omp sessions (default ${DEFAULT_CAPS.maxConcurrentWorkers})`),
111
+ maxConcurrentWorkersPerRepo: z.number().min(0).describe(`Max live workers per repository (default ${DEFAULT_CAPS.maxConcurrentWorkersPerRepo})`),
112
+ dailySpendUsd: z
113
+ .number()
114
+ .min(0)
115
+ .nullable()
116
+ .describe(`Rolling-day spend ceiling; null means no spend gate (default ${DEFAULT_CAPS.dailySpendUsd})`),
117
+ planUsage: z.union([planUsageCap, z.null()]).describe(`Plan-allowance guard, or null for unmetered`),
118
+ workerMaxTurns: z.number().min(0).describe(`Turn ceiling for one worker (default ${DEFAULT_CAPS.workerMaxTurns})`),
119
+ workerMaxTurnsCeiling: z.number().min(0).describe(`Maximum turn budget assignable to one issue's next attempt`),
120
+ workerWallClockMs: z.number().min(0).describe(`Wall-clock ceiling for one worker (default ${DEFAULT_CAPS.workerWallClockMs})`),
121
+ maxAttemptsPerIssue: z.number().min(0).describe(`Failed attempts allowed before escalation (default ${DEFAULT_CAPS.maxAttemptsPerIssue})`),
122
+ maxContinuationsPerIssue: z.number().min(0).describe(`Operational continuations allowed before crash/resume escalation (default ${DEFAULT_CAPS.maxContinuationsPerIssue})`),
123
+ })
124
+ // A cap object carries any subset (project overrides are partial) and may
125
+ // carry retired keys (v1) that the load boundary drops or rejects by version,
126
+ // so unknown keys are kept and reconciled in `config.ts`, never rejected here.
127
+ .partial()
128
+ .loose()
129
+ .describe("Per-fleet or per-project hard limits");
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Reporting policy
133
+ // ---------------------------------------------------------------------------
134
+
135
+ const availabilitySchema = z
136
+ .object({
137
+ timezone: z.string().min(1).describe("IANA timezone of the window"),
138
+ days: z.array(weekdayEnum).min(1).describe("Days the window is open"),
139
+ start: z.string().regex(HHMM, "must be a 24h HH:MM time").describe("Window open time, operator-local"),
140
+ end: z.string().regex(HHMM, "must be a 24h HH:MM time").describe("Window close time, operator-local"),
141
+ bypass: z.array(interruptCategoryEnum).describe("Categories that may interrupt outside the window").optional(),
142
+ })
143
+ .strict()
144
+ .superRefine((a, ctx) => {
145
+ // Presence is required on disk (unlike a defaultable field); report the
146
+ // missing key in the loader's own voice rather than zod's.
147
+ if (a.bypass === undefined) {
148
+ ctx.addIssue({
149
+ code: "custom",
150
+ path: ["bypass"],
151
+ message: `must be an array of ${INTERRUPT_CATEGORY_LIST} (empty means none)`,
152
+ });
153
+ }
154
+ });
155
+
156
+ const digestSchema = z
157
+ .object({
158
+ cadence: digestCadenceEnum.describe("When the daily/report rollup is due"),
159
+ at: z.string().regex(HHMM, "must be a 24h HH:MM time").optional().describe("Daily rollup time"),
160
+ timezone: z.string().describe("IANA timezone of the daily rollup").optional(),
161
+ })
162
+ .strict()
163
+ .describe("The reporting digest/rollup schedule");
164
+
165
+ const reportingSchema = z
166
+ .object({
167
+ scope: reportScopeEnum.optional().describe("Legacy reporting preset; mutually exclusive with the explicit form"),
168
+ scopePreset: reportScopeEnum.optional().describe("Back-annotation of the legacy preset this policy came from"),
169
+ interruptOn: z.array(interruptCategoryEnum).optional().describe("Which categories interrupt the operator's phone"),
170
+ digest: digestSchema.optional(),
171
+ availability: availabilitySchema.optional(),
172
+ })
173
+ .strict();
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Merge / release preconditions
177
+ // ---------------------------------------------------------------------------
178
+
179
+ const nameListSchema = z.array(z.string().min(1));
180
+
181
+ const mergePreconditionsSchema = z
182
+ .object({
183
+ requiredChecks: nameListSchema.default(DEFAULT_PROJECT_POLICY.merge.requiredChecks),
184
+ baseFreshness: baseFreshnessEnum.default(DEFAULT_PROJECT_POLICY.merge.baseFreshness),
185
+ drafts: draftPolicyEnum.default(DEFAULT_PROJECT_POLICY.merge.drafts),
186
+ whenBehindBase: behindBaseActionEnum.default(DEFAULT_PROJECT_POLICY.merge.whenBehindBase),
187
+ })
188
+ .strict();
189
+
190
+ const releasePreconditionsSchema = z
191
+ .object({
192
+ requires: z.array(releaseRequirementEnum).default(DEFAULT_PROJECT_POLICY.release.requires),
193
+ requiredChecks: nameListSchema.default(DEFAULT_PROJECT_POLICY.release.requiredChecks),
194
+ artefacts: nameListSchema.default(DEFAULT_PROJECT_POLICY.release.artefacts),
195
+ environments: nameListSchema.default(DEFAULT_PROJECT_POLICY.release.environments),
196
+ })
197
+ .strict();
198
+
199
+ const projectPolicySchema = z
200
+ .object({
201
+ merge: mergePreconditionsSchema.optional(),
202
+ release: releasePreconditionsSchema.optional(),
203
+ })
204
+ .strict();
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Authority / escalation / release policy
208
+ // ---------------------------------------------------------------------------
209
+
210
+ const authoritySchema = z
211
+ .object({
212
+ merge: authorityHolderEnum.default(DEFAULT_AUTHORITY.merge),
213
+ release: authorityHolderEnum.default(DEFAULT_AUTHORITY.release),
214
+ })
215
+ .strict();
216
+
217
+ const escalationSchema = z
218
+ .object({
219
+ telegramChatId: z.unknown().optional(),
220
+ telegramTopicId: z.unknown().optional(),
221
+ fallbackToIssueComment: z.unknown().default(true),
222
+ orchestrator: orchestratorModeEnum.default("embedded"),
223
+ })
224
+ .strict();
225
+
226
+ const releasePolicySchema = z.union([
227
+ releasePolicyLegacyEnum,
228
+ z.record(z.string(), authorityHolderEnum),
229
+ ]);
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Repo targets
233
+ // ---------------------------------------------------------------------------
234
+
235
+ const gateSchema = z
236
+ .object({
237
+ cmd: z.string().optional(),
238
+ cwd: z.unknown().default("."),
239
+ })
240
+ .superRefine((g, ctx) => {
241
+ if (typeof g.cmd !== "string" || g.cmd.trim() === "") {
242
+ ctx.addIssue({ code: "custom", message: `must be { cmd, cwd } with a non-empty cmd` });
243
+ }
244
+ });
245
+
246
+ const gatesSchema = z.array(gateSchema);
247
+
248
+ const repoTargetSchema = z
249
+ .object({
250
+ name: z.unknown().optional(),
251
+ cloneUrl: z.string().min(1, "must be a non-empty string"),
252
+ defaultBranch: z.unknown().default("main").optional(),
253
+ gates: gatesSchema.optional(),
254
+ graphProject: z.string().optional(),
255
+ migrations: z
256
+ .object({ dir: z.string().min(1, "must be a non-empty string") })
257
+ .strict()
258
+ .optional(),
259
+ })
260
+ .strict();
261
+
262
+ const stateLabelsSchema = z
263
+ .object({
264
+ // The loader defaults each of these (pickString), so a malformed value is
265
+ // tolerated rather than rejected — the schema admits any shape and lets the
266
+ // normaliser decide what is usable.
267
+ inProgress: z.unknown(),
268
+ blocked: z.unknown(),
269
+ failed: z.unknown(),
270
+ })
271
+ .partial();
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Project / root
275
+ // ---------------------------------------------------------------------------
276
+
277
+ const routingSchema = z.object({
278
+ labelPrefix: z.unknown().optional(),
279
+ repos: z
280
+ .record(z.string(), repoTargetSchema)
281
+ .refine((r) => Object.keys(r).length > 0, `routing.repos needs at least one repo entry, or no issue can be routed`),
282
+ });
283
+
284
+ const projectSchema = z
285
+ .object({
286
+ name: z.string().min(1, "must be a non-empty string"),
287
+ tracker: z
288
+ .object({
289
+ kind: z.literal("github").optional(),
290
+ repo: z.string().regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, `must look like "owner/repo"`),
291
+ })
292
+ .strict(),
293
+ queueLabel: z.string().min(1, "must be a non-empty string — it is the human sign-off gate"),
294
+ groomBelow: z.unknown().optional(),
295
+ stateLabels: stateLabelsSchema.optional(),
296
+ routing: routingSchema.optional(),
297
+ caps: capsSchema.optional(),
298
+ workerModel: z.unknown().optional(),
299
+ escalation: escalationSchema.optional(),
300
+ authority: authoritySchema.optional(),
301
+ releasePolicy: releasePolicySchema.optional(),
302
+ policy: projectPolicySchema.optional(),
303
+ reporting: reportingSchema.optional(),
304
+ // Roots silently fall back to the default when unusable, so the schema
305
+ // admits any shape and the normaliser picks the usable path.
306
+ workspaceRoot: z.unknown().optional(),
307
+ mirrorRoot: z.unknown().optional(),
308
+ })
309
+ .loose()
310
+ .describe("One product this conductor services");
311
+
312
+ const configSchema = z
313
+ .object({
314
+ $schema: z.string().optional().describe("Path to the shipped config.schema.json"),
315
+ version: z.number().describe(`The config format version (${READABLE_CONFIG_VERSIONS.join(" or ")})`),
316
+ defaults: capsSchema.optional(),
317
+ projects: z.array(projectSchema).min(1, `"projects" must be a non-empty array — the dispatcher has nothing to service otherwise`),
318
+ })
319
+ .loose()
320
+ .describe("On-disk configuration for omp-conductor");
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Emitted schema
324
+ // ---------------------------------------------------------------------------
325
+
326
+ /** The zod schema `loadConfig` parses the on-disk config with. */
327
+ export const ConfigSchema = configSchema;
328
+
329
+ /**
330
+ * The draft 2020-12 JSON Schema rendering of {@link ConfigSchema}, shipped to
331
+ * `schema/config.schema.json` (see the `bun run schema` script) so editors can
332
+ * validate a hand-written config against the same vocabulary the loader enforces.
333
+ */
334
+ export function configJsonSchema(): object {
335
+ return z.toJSONSchema(ConfigSchema) as object;
336
+ }
337
+
338
+ // Re-exports the renderer consumes without duplicating the lists.
339
+ export {
340
+ REPORT_SCOPE_LIST,
341
+ INTERRUPT_CATEGORY_LIST,
342
+ WEEKDAY_LIST,
343
+ DIGEST_CADENCE_LIST,
344
+ AUTHORITY_HOLDER_LIST,
345
+ BASE_FRESHNESS_LIST,
346
+ DRAFT_POLICY_LIST,
347
+ BEHIND_BASE_ACTION_LIST,
348
+ RELEASE_REQUIREMENT_LIST,
349
+ RELEASE_SHAPE_LIST,
350
+ ORCHESTRATOR_MODE_LIST,
351
+ LEGACY_RELEASE_POLICY_LIST,
352
+ };