omp-conductor 0.18.1 → 0.19.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 (69) hide show
  1. package/README.md +106 -41
  2. package/REFERENCE.md +866 -31
  3. package/agents/to-spec.md +6 -2
  4. package/package.json +1 -1
  5. package/schema/config.schema.json +32 -1
  6. package/src/admission.ts +212 -26
  7. package/src/arm-challenge.ts +250 -57
  8. package/src/ask.ts +288 -1
  9. package/src/briefs/orchestrator.md +27 -13
  10. package/src/briefs/to-spec.md +6 -2
  11. package/src/cli.ts +127 -2
  12. package/src/command-help.ts +9 -1
  13. package/src/command-manifest.ts +52 -8
  14. package/src/commands/arm.ts +6 -2
  15. package/src/commands/context.ts +2 -0
  16. package/src/commands/intake.ts +4 -19
  17. package/src/commands/message.ts +26 -2
  18. package/src/commands/reconcile-units.ts +104 -0
  19. package/src/commands/release-composition.ts +232 -0
  20. package/src/commands/resume.ts +2 -27
  21. package/src/commands/setup.ts +101 -16
  22. package/src/commands/stats.ts +11 -30
  23. package/src/commands/tail.ts +31 -1
  24. package/src/commands/upgrade.ts +20 -3
  25. package/src/commands/verb.ts +2 -1
  26. package/src/commands/watch.ts +4 -17
  27. package/src/config-schema.ts +38 -6
  28. package/src/config.ts +103 -8
  29. package/src/credential-class.ts +366 -0
  30. package/src/daemon.ts +1368 -529
  31. package/src/dashboard/app.js +504 -2
  32. package/src/dashboard/controls.ts +336 -0
  33. package/src/dashboard/index.html +30 -0
  34. package/src/dashboard/server.ts +271 -30
  35. package/src/dashboard/style.css +116 -0
  36. package/src/dashboard/transcript.ts +173 -0
  37. package/src/decisions.ts +19 -11
  38. package/src/doctor.ts +431 -148
  39. package/src/escalate.ts +22 -11
  40. package/src/failure-class.ts +59 -0
  41. package/src/fleet.ts +587 -230
  42. package/src/host.ts +6 -455
  43. package/src/omp-settings.ts +19 -0
  44. package/src/omp.ts +40 -56
  45. package/src/orchestrator-tick.ts +564 -121
  46. package/src/pause.ts +233 -0
  47. package/src/session-host.ts +6 -41
  48. package/src/settlement.ts +159 -2
  49. package/src/setup-answers.ts +97 -0
  50. package/src/setup-host.ts +343 -1160
  51. package/src/setup-install.ts +204 -27
  52. package/src/setup-wizard.ts +252 -51
  53. package/src/setup.ts +87 -4
  54. package/src/spend-telemetry.ts +117 -0
  55. package/src/stats.ts +35 -0
  56. package/src/status-render.ts +485 -19
  57. package/src/store.ts +1229 -55
  58. package/src/telegram-freshness.ts +269 -0
  59. package/src/to-spec.ts +50 -2
  60. package/src/types.ts +759 -10
  61. package/src/unblock.ts +22 -0
  62. package/src/unit-reconcile.ts +303 -0
  63. package/src/upgrade-verify.ts +8 -1
  64. package/src/upgrade.ts +299 -12
  65. package/src/verbs/actions.ts +124 -10
  66. package/src/verbs/protocol.ts +70 -2
  67. package/src/verbs/server.ts +485 -11
  68. package/src/wake.ts +48 -0
  69. package/src/worker.ts +401 -14
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Is the daily spend cap's own input still arriving? (#970)
3
+ *
4
+ * The cap compares `store.spendSince(project, startOfToday())` — a sum of the
5
+ * per-run `spendUsd` column — against its ceiling. When the harness stops
6
+ * reporting cost, that column reads 0.00 and the cap silently measures a
7
+ * fraction of reality. #46 named this once already: **$0.00 spend is not proof
8
+ * of no spend.**
9
+ *
10
+ * Measured on this fleet, counting only runs that actually did work:
11
+ *
12
+ * | day | worked | reported $0.00 |
13
+ * |------------|--------|----------------|
14
+ * | 2026-08-15 | 53 | 3.8% |
15
+ * | … | | 0–2.5% |
16
+ * | 2026-08-20 | 15 | 0.0% |
17
+ * | 2026-08-21 | 22 | 31.8% |
18
+ * | 2026-08-22 | 12 | 75.0% |
19
+ *
20
+ * Six days of ≤3.8% baseline, then 32%, then 75% — with #614 running 213 turns
21
+ * for $0.00.
22
+ *
23
+ * Two properties are load-bearing, and both were wrong in the predicate this
24
+ * replaces:
25
+ *
26
+ * - **Only runs that did work are judged.** A run killed before its first turn
27
+ * records $0.00 honestly; 26 of this project's 48 zero-spend rows are exactly
28
+ * that. Counting them inflates the ratio *and* lets a run of administrative
29
+ * kills fire a telemetry finding that says nothing about telemetry.
30
+ * - **A ratio, not `every`.** The old check fired only when every sampled run
31
+ * reported zero. Replayed over all 410 windows in this fleet's history it
32
+ * would have fired 14 times while staying silent through 37 windows that had
33
+ * lost a majority of their telemetry — so the common case, partial loss, was
34
+ * the invisible one. On 2026-08-21, at 31.8% loss, every window that day
35
+ * contained one metered run and the probe was silent all day.
36
+ *
37
+ * One judgement, two consumers: `doctor`'s finding and the `status` row. The
38
+ * cap's figure and the statement about whether that figure means anything must
39
+ * never come from two implementations that can disagree.
40
+ */
41
+
42
+ /** One sampled run, reduced to what the judgement needs. */
43
+ export interface SpendSample {
44
+ /** Turns taken. Zero means the run never worked, so its $0.00 is honest. */
45
+ turns: number;
46
+ spendUsd: number;
47
+ }
48
+
49
+ /**
50
+ * The share of *working* runs whose spend is missing at or above which this is
51
+ * reported.
52
+ *
53
+ * A majority, deliberately — the observed healthy baseline is 0–3.8%, not zero.
54
+ * A legitimately free or aborted-after-one-turn run happens, and a check that
55
+ * fires on one of them is a check that gets ignored, which is the failure the
56
+ * install-surface rows already name: a warning repeated every fifteen minutes
57
+ * trains an operator to ignore the row that matters.
58
+ */
59
+ export const SPEND_MISSING_SHARE = 0.5;
60
+
61
+ export type SpendTelemetryVerdict =
62
+ /** Enough working runs, and most of them metered. */
63
+ | { kind: "healthy"; worked: number; missing: number; totalUsd: number }
64
+ /** Not enough working runs to say anything — never a pass by default. */
65
+ | { kind: "insufficient"; worked: number; needed: number }
66
+ /** A majority of working runs reported nothing. */
67
+ | { kind: "partial"; worked: number; missing: number }
68
+ /** Every working run reported nothing — the same fact, stated louder. */
69
+ | { kind: "absent"; worked: number };
70
+
71
+ /**
72
+ * Judge a newest-first sample.
73
+ *
74
+ * `limit` is how many *working* runs are wanted, not how many rows to read: the
75
+ * caller over-samples and this takes the first `limit` runs that did work, so a
76
+ * burst of 0-turn kills cannot push the real evidence out of the window.
77
+ */
78
+ export function judgeSpendTelemetry(
79
+ samples: readonly SpendSample[],
80
+ limit: number,
81
+ ): SpendTelemetryVerdict {
82
+ const working = samples.filter((row) => row.turns > 0).slice(0, limit);
83
+ if (working.length < limit) {
84
+ return { kind: "insufficient", worked: working.length, needed: limit };
85
+ }
86
+ const missing = working.filter((row) => row.spendUsd === 0).length;
87
+ if (missing === working.length) return { kind: "absent", worked: working.length };
88
+ if (missing / working.length >= SPEND_MISSING_SHARE) {
89
+ return { kind: "partial", worked: working.length, missing };
90
+ }
91
+ return {
92
+ kind: "healthy",
93
+ worked: working.length,
94
+ missing,
95
+ totalUsd: working.reduce((sum, row) => sum + row.spendUsd, 0),
96
+ };
97
+ }
98
+
99
+ /** The operator-facing sentence, or undefined when there is nothing to say.
100
+ * Shared so `doctor` and `status` cannot word the same fact differently. */
101
+ export function spendTelemetryDetail(verdict: SpendTelemetryVerdict): string | undefined {
102
+ switch (verdict.kind) {
103
+ case "absent":
104
+ return (
105
+ `the last ${verdict.worked} completed runs that did any work all recorded $0.00 spend` +
106
+ " — the USD cap cannot fire on zeros, and $0.00 is not proof of no spend"
107
+ );
108
+ case "partial":
109
+ return (
110
+ `${verdict.missing} of the last ${verdict.worked} completed runs that did any work recorded ` +
111
+ "$0.00 spend — the USD cap is comparing a fraction of actual spend against its ceiling"
112
+ );
113
+ case "healthy":
114
+ case "insufficient":
115
+ return undefined;
116
+ }
117
+ }
package/src/stats.ts CHANGED
@@ -19,6 +19,7 @@
19
19
  * counted separately as **unmetered** and never averaged into a per-issue cost.
20
20
  */
21
21
 
22
+ import { utcDay } from "./store.ts";
22
23
  import type { RunRecord } from "./types.ts";
23
24
 
24
25
  /** The bounded window a report answers over. Day keys are UTC (`YYYY-MM-DD`). */
@@ -181,6 +182,40 @@ function summarize(args: {
181
182
  * settled there. Rows within an issue must be ordered by `startedAt`, so the
182
183
  * first row of a chain is its first queue-label claim.
183
184
  */
185
+ /** `7d` / `30d` / any positive whole-day duration. */
186
+ const DURATION_FORM = /^(\d+)d$/;
187
+ /** A bare UTC date: the window starts at 00:00:00Z that day. */
188
+ const DATE_FORM = /^\d{4}-\d{2}-\d{2}$/;
189
+ const DAY_MS = 86_400_000;
190
+
191
+ /**
192
+ * `--since` (or `?since=`) to a full window, or `undefined` when it is not one.
193
+ *
194
+ * Here rather than in the CLI because the dashboard needs the identical parse
195
+ * (#297): two parsers would eventually disagree about what `30d` means, and the
196
+ * page's numbers are supposed to equal `stats --json` for the same window. The
197
+ * CLI turns `undefined` into its exit-2 message and the endpoint turns it into a
198
+ * 400 — neither silently defaults a typo'd window, because measuring the wrong
199
+ * week and saying nothing is the failure this shape prevents.
200
+ */
201
+ export function parseStatsWindow(raw: string, now: number): StatsWindow | undefined {
202
+ const until = { untilEpochMs: now, untilDay: utcDay(now) };
203
+ const duration = DURATION_FORM.exec(raw);
204
+ if (duration !== null) {
205
+ const days = Number(duration[1]);
206
+ if (days >= 1) {
207
+ const sinceEpochMs = now - days * DAY_MS;
208
+ return { sinceEpochMs, sinceDay: utcDay(sinceEpochMs), ...until };
209
+ }
210
+ return undefined;
211
+ }
212
+ if (DATE_FORM.test(raw)) {
213
+ const sinceEpochMs = Date.parse(`${raw}T00:00:00Z`);
214
+ if (Number.isFinite(sinceEpochMs)) return { sinceEpochMs, sinceDay: raw, ...until };
215
+ }
216
+ return undefined;
217
+ }
218
+
184
219
  export function computeStats(args: {
185
220
  project: string;
186
221
  window: StatsWindow;