omp-conductor 0.15.9 → 0.15.11

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 (86) hide show
  1. package/README.md +273 -2543
  2. package/REFERENCE.md +2638 -0
  3. package/package.json +3 -2
  4. package/schema/config.schema.json +8 -23
  5. package/src/arm-challenge.ts +112 -0
  6. package/src/ask.ts +434 -0
  7. package/src/board.ts +81 -15
  8. package/src/brief-upgrade.ts +114 -8
  9. package/src/briefs/orchestrator.md +55 -29
  10. package/src/briefs/policy.md +14 -5
  11. package/src/briefs/worker.md +7 -1
  12. package/src/chain-check.ts +1 -1
  13. package/src/check-trailing-newlines.ts +82 -0
  14. package/src/cli.ts +190 -1391
  15. package/src/commands/arm.ts +21 -0
  16. package/src/commands/board.ts +23 -0
  17. package/src/commands/brief-upgrade.ts +186 -0
  18. package/src/commands/context.ts +49 -0
  19. package/src/commands/daemon.ts +71 -0
  20. package/src/commands/dashboard.ts +74 -0
  21. package/src/commands/decision.ts +103 -0
  22. package/src/commands/disarm.ts +21 -0
  23. package/src/commands/doctor.ts +98 -0
  24. package/src/commands/event.ts +62 -0
  25. package/src/commands/extend.ts +64 -0
  26. package/src/commands/friction.ts +56 -0
  27. package/src/commands/help.ts +9 -0
  28. package/src/commands/hold.ts +26 -0
  29. package/src/commands/intake.ts +134 -0
  30. package/src/commands/ledger.ts +69 -0
  31. package/src/commands/message.ts +48 -0
  32. package/src/commands/report.ts +170 -0
  33. package/src/commands/restart.ts +76 -0
  34. package/src/commands/resume.ts +58 -0
  35. package/src/commands/setup.ts +93 -0
  36. package/src/commands/start.ts +23 -0
  37. package/src/commands/stats.ts +131 -0
  38. package/src/commands/status.ts +48 -0
  39. package/src/commands/stop.ts +51 -0
  40. package/src/commands/tail.ts +109 -0
  41. package/src/commands/unblock.ts +39 -0
  42. package/src/commands/upgrade-install.ts +31 -0
  43. package/src/commands/upgrade-rollback.ts +23 -0
  44. package/src/commands/upgrade.ts +25 -0
  45. package/src/commands/verb.ts +83 -0
  46. package/src/commands/version.ts +30 -0
  47. package/src/commands/worker.ts +100 -0
  48. package/src/config-schema.ts +38 -1
  49. package/src/config.ts +10 -3
  50. package/src/daemon.ts +613 -94
  51. package/src/dashboard/app.js +120 -0
  52. package/src/dashboard/index.html +34 -0
  53. package/src/dashboard/server.ts +267 -0
  54. package/src/dashboard/style.css +180 -0
  55. package/src/decisions.ts +39 -14
  56. package/src/diff-flags.ts +131 -241
  57. package/src/doctor.ts +795 -0
  58. package/src/escalate.ts +60 -19
  59. package/src/failure-class.ts +29 -3
  60. package/src/fleet.ts +58 -1
  61. package/src/graph-health.ts +1 -1
  62. package/src/label-projection.ts +1 -1
  63. package/src/lifecycle.ts +198 -2
  64. package/src/notices.ts +9 -0
  65. package/src/omp.ts +2 -0
  66. package/src/orchestrator-tick.ts +315 -17
  67. package/src/release-policy.ts +135 -23
  68. package/src/reports.ts +19 -5
  69. package/src/setup-host.ts +420 -8
  70. package/src/setup-install.ts +69 -14
  71. package/src/setup-wizard.ts +199 -61
  72. package/src/setup.ts +131 -35
  73. package/src/stats.ts +331 -0
  74. package/src/store.ts +206 -21
  75. package/src/tracker/github.ts +27 -4
  76. package/src/types.ts +144 -31
  77. package/src/unblock.ts +55 -11
  78. package/src/upgrade-journal.ts +220 -0
  79. package/src/upgrade-verify.ts +506 -0
  80. package/src/upgrade.ts +295 -26
  81. package/src/verbs/actions.ts +73 -1
  82. package/src/verbs/protocol.ts +29 -4
  83. package/src/verbs/server.ts +183 -20
  84. package/systemd/omp-conductor-recover.sh +433 -0
  85. package/systemd/omp-conductor.service.example +7 -0
  86. package/systemd/recover-unit-test.sh +428 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * The durable journal for fleet-initiated upgrades (#486).
3
+ *
4
+ * `omp-conductor upgrade` has always been a transaction *inside* one process:
5
+ * it paused, drained, installed, reloaded and verified, and the process that
6
+ * verified was the process that restarted the fleet. The fleet-installs-itself
7
+ * path replaces that wrapper, not the transaction: a detached transient unit
8
+ * performs the install while this module records every surface it touches, and
9
+ * the first tick of the post-restart daemon verifies against the journal —
10
+ * a process that did not run the install, which is the only witness worth
11
+ * trusting after the restarts.
12
+ *
13
+ * Everything here is a leaf on purpose: the verb server, the daemon's
14
+ * privileged actions and the upgrade engine all need the same journal and the
15
+ * same transient-unit naming, and importing `upgrade.ts` from both verbs and
16
+ * daemon would close import cycles that have no business existing. The module
17
+ * holds no state of its own; the journal is append-only JSONL under the
18
+ * conductor state directory, so a process that died mid-write leaves every
19
+ * line before the torn one readable.
20
+ */
21
+
22
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { stateDir } from "./config.ts";
25
+
26
+ /**
27
+ * Where the fleet's upgrade journal lives: one JSONL file per host, because
28
+ * the transaction it records is host-wide — one daemon serves every configured
29
+ * project, and the install replaces every surface at once.
30
+ */
31
+ export const UPGRADE_JOURNAL_FILE = "upgrade-journal.jsonl";
32
+
33
+ /** The packages this fleet never leaves on mixed versions. */
34
+ export interface JournalSurfaces {
35
+ cliVersion: string;
36
+ ompVersion?: string;
37
+ herdrSource?: string;
38
+ }
39
+
40
+ /** What the fleet looked like when the install began, for delta verification. */
41
+ export interface JournalFleetState {
42
+ dispatch: string;
43
+ ticks: string;
44
+ pane: string;
45
+ herdr: string;
46
+ }
47
+
48
+ /** One named verification check, recorded so the outcome is evidence, not prose. */
49
+ export interface UpgradeCheck {
50
+ name: string;
51
+ ok: boolean;
52
+ detail?: string;
53
+ }
54
+
55
+ export interface UpgradeJournalEntry {
56
+ /** ISO timestamp of the write. */
57
+ at: string;
58
+ kind: "request" | "snapshot" | "phase" | "outcome";
59
+ /** Always present for request/outcome; phases carry them for searchability. */
60
+ version?: string;
61
+ gitHead?: string;
62
+ /** The transient unit performing the install, when it named itself. */
63
+ unit?: string;
64
+ /** The step name: paused, drain, install, brief, restart, awaiting-verification, … */
65
+ phase?: string;
66
+ /** The surface one phase touched: cli/omp/herdr/brief/herdr-service/daemon/dispatch. */
67
+ surface?: string;
68
+ ok: boolean;
69
+ detail?: string;
70
+ /** The durable pre-install identities, for the process that must roll back. */
71
+ previous?: InstallSnapshotSurfaces;
72
+ configBackup?: string;
73
+ /** The pause sentinel the install owns, for the verifier that must lift it. */
74
+ pauseKey?: string;
75
+ /** Whether dispatch was already paused when the request began. */
76
+ initialPaused?: boolean;
77
+ /** The fleet layers the install began under, for delta verification. */
78
+ initial?: JournalFleetState;
79
+ /** The projects the install covers, in the form the upgrade engine uses. */
80
+ selectors?: readonly (string | null)[];
81
+ /** Verification evidence when the post-restart tick closes the transaction. */
82
+ checks?: readonly UpgradeCheck[];
83
+ }
84
+
85
+ /** The installed identities a rollback must restore, verbatim. */
86
+ export interface InstallSnapshotSurfaces {
87
+ cliVersion: string;
88
+ ompVersion?: string;
89
+ herdrSource?: string;
90
+ }
91
+
92
+ export function upgradeJournalPath(root = stateDir()): string {
93
+ return join(root, UPGRADE_JOURNAL_FILE);
94
+ }
95
+
96
+ export function readUpgradeJournal(root = stateDir()): UpgradeJournalEntry[] {
97
+ const path = upgradeJournalPath(root);
98
+ if (!existsSync(path)) return [];
99
+ let text: string;
100
+ try {
101
+ text = readFileSync(path, "utf8");
102
+ } catch {
103
+ // An unreadable journal is the same answer as an absent one for the read
104
+ // side; the writer keeps appending and the operator can recover the file.
105
+ return [];
106
+ }
107
+ const entries: UpgradeJournalEntry[] = [];
108
+ for (const line of text.split("\n")) {
109
+ if (line.length === 0) continue;
110
+ try {
111
+ const entry = JSON.parse(line) as UpgradeJournalEntry;
112
+ if (entry !== null && typeof entry === "object") entries.push(entry);
113
+ } catch {
114
+ // One torn line (the process died mid-append) does not hide the earlier
115
+ // evidence; it is exactly the "interrupted transaction" the journal was
116
+ // built to diagnose.
117
+ }
118
+ }
119
+ return entries;
120
+ }
121
+
122
+ /**
123
+ * Append one line. Each write is one `appendFileSync`, so a crash cannot
124
+ * interleave two entries; callers order their own phases.
125
+ */
126
+ export function appendJournal(root: string, entry: UpgradeJournalEntry): void {
127
+ mkdirSync(root, { recursive: true });
128
+ appendFileSync(
129
+ join(root, UPGRADE_JOURNAL_FILE),
130
+ `${JSON.stringify(entry)}\n`,
131
+ { mode: 0o600 },
132
+ );
133
+ }
134
+
135
+ /**
136
+ * The transient systemd unit that performs an install or its rollback. The
137
+ * unit lives in the system manager, not in the pane's process tree and not in
138
+ * the daemon's cgroup, so `systemctl restart herdr-fleet.service` and the
139
+ * daemon restart both pass it by. The version is slugged into the unit name
140
+ * because the unit names the transaction; a second request for the same
141
+ * version collides with the first on purpose (see the launch guard).
142
+ */
143
+ export function transientUnitName(version: string, kind: "install" | "rollback"): string {
144
+ const slug = version.replace(/[^a-zA-Z0-9_.-]/g, "-");
145
+ return `omp-conductor-upgrade-${kind}-${slug}`;
146
+ }
147
+
148
+ /**
149
+ * The explicit PATH a detached unit must carry. On this fleet's host `omp`,
150
+ * `herdr` and `omp-conductor` are absent from the non-interactive ssh PATH,
151
+ * and a transient unit starts with systemd's own minimal PATH — so the
152
+ * executor prepends the two well-known runtime dirs and keeps whatever the
153
+ * daemon itself ran with. Deduplicated so a dir already on the path is not
154
+ * listed twice.
155
+ */
156
+ const EXECUTOR_PATH_DIRS = ["/root/.bun/bin", "/root/.local/bin"];
157
+
158
+ export function executorPath(env: NodeJS.ProcessEnv): string {
159
+ const existing = typeof env["PATH"] === "string" ? env["PATH"] : "";
160
+ const parts = [...EXECUTOR_PATH_DIRS, ...existing.split(":").filter((part) => part.length > 0)];
161
+ return [...new Set(parts)].join(":");
162
+ }
163
+
164
+ export type UnitSpawnResult = { code: number; stdout: string; stderr: string };
165
+ export type UnitSpawnFn = (argv: readonly string[]) => Promise<UnitSpawnResult>;
166
+
167
+ export type UnitCommand =
168
+ | { kind: "install"; version: string }
169
+ | { kind: "rollback"; version: string };
170
+
171
+ export interface LaunchedUnit {
172
+ ok: true;
173
+ unit: string;
174
+ }
175
+ export interface UnitLaunchRefusal {
176
+ ok: false;
177
+ stderr: string;
178
+ }
179
+
180
+ /**
181
+ * Start one detached transient unit that performs an install or a rollback.
182
+ *
183
+ * The environment is explicit and minimal on purpose: `PATH` is composed by
184
+ * {@link executorPath} so the unit can find `omp`, `herdr` and
185
+ * `omp-conductor`, and the requesting context's `HERDR_SESSION` (if any) is
186
+ * passed through so the fleet the unit talks to is the fleet the request
187
+ * meant. The unit is `--collect`ed so a finished unit leaves no failed-unit
188
+ * litter behind. The spawning process keeps the unit name; the unit is told
189
+ * through `OMP_CONDUCTOR_UNIT` so its journal records which unit ran.
190
+ */
191
+ export async function launchTransientUnit(
192
+ spawn: UnitSpawnFn,
193
+ env: NodeJS.ProcessEnv,
194
+ command: UnitCommand,
195
+ ): Promise<LaunchedUnit | UnitLaunchRefusal> {
196
+ const unit = transientUnitName(command.version, command.kind);
197
+ const setenv = [`PATH=${executorPath(env)}`, `OMP_CONDUCTOR_UNIT=${unit}`];
198
+ const session = env["HERDR_SESSION"];
199
+ if (session !== undefined && session.length > 0) setenv.push(`HERDR_SESSION=${session}`);
200
+ const argv = [
201
+ "systemd-run",
202
+ `--unit=${unit}`,
203
+ "--collect",
204
+ ...setenv.map((pair) => `--setenv=${pair}`),
205
+ "omp-conductor",
206
+ command.kind === "install" ? "upgrade-install" : "upgrade-rollback",
207
+ ...(command.kind === "install" ? ["--to", command.version] : []),
208
+ ];
209
+ const ran = await spawn(argv);
210
+ if (ran.code !== 0) {
211
+ const detail = ran.stderr.trim() || ran.stdout.trim() || `exit ${ran.code}`;
212
+ return {
213
+ ok: false,
214
+ stderr:
215
+ `could not start ${unit} through systemd-run: ${detail}. ` +
216
+ "Nothing was installed and dispatch was not touched; run `omp-conductor upgrade` from a shell instead.",
217
+ };
218
+ }
219
+ return { ok: true, unit };
220
+ }