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
package/src/escalate.ts CHANGED
@@ -1040,13 +1040,14 @@ export interface TelegramPlumbingProbe {
1040
1040
  }
1041
1041
 
1042
1042
  /**
1043
- * The session surface the arm waiter actually scans for the challenge reply:
1044
- * {@link armTicks} polls every `.jsonl` transcript *directly inside* these
1045
- * directories — the tick-cwd-derived session directory plus, when the
1046
- * project's claim names a session file elsewhere, that file's directory. The
1047
- * recipient of the reply (a claim's, the DM owner's, or the local poll
1048
- * owner's session file) must land in this set, or the reply can never satisfy
1049
- * the challenge even though inbound works.
1043
+ * The transcript-surface input for {@link telegramPlumbingVerdict}'s session
1044
+ * identity checks: the directories the claim-only proof judges recipients
1045
+ * against (a claim's, the DM owner's, or the local poll owner's session file
1046
+ * must land inside, or a flat reply could land where no challenge would have
1047
+ * looked). Since #614 the *challenge* proof reads conductor acknowledgement
1048
+ * state and never opens a transcript, so callers judging challenge-mode
1049
+ * plumbing omit this — passing it there could report arming impossible while
1050
+ * `arm` succeeds.
1050
1051
  */
1051
1052
  export type TelegramPlumbingScan = { dirs: readonly string[] };
1052
1053
 
@@ -1074,7 +1075,7 @@ export type TelegramPlumbingScan = { dirs: readonly string[] };
1074
1075
  */
1075
1076
  export function telegramPlumbingVerdict(
1076
1077
  sendTopic: number | undefined,
1077
- scan: TelegramPlumbingScan,
1078
+ scan: TelegramPlumbingScan | undefined,
1078
1079
  probe: TelegramPlumbingProbe,
1079
1080
  ): TelegramPlumbingVerdict {
1080
1081
  const { registry, poll, dmOwner, channel, alive, lockAlive, fresh } = probe;
@@ -1085,14 +1086,21 @@ export function telegramPlumbingVerdict(
1085
1086
  const claim = registry.claims.find((c) => c.threadId === sendTopic);
1086
1087
  if (claim === undefined) return { ok: false, reason: "no-topic-claim" };
1087
1088
  if (claim.pid === undefined || !alive(claim.pid)) return { ok: false, reason: "claim-dead" };
1088
- if (claim.sessionFile !== undefined && !sessionWithinScan(claim.sessionFile, scan.dirs)) {
1089
+ if (
1090
+ scan !== undefined &&
1091
+ claim.sessionFile !== undefined &&
1092
+ !sessionWithinScan(claim.sessionFile, scan.dirs)
1093
+ ) {
1089
1094
  return { ok: false, reason: "claim-session-outside" };
1090
1095
  }
1091
1096
  recipient = "claim";
1092
1097
  } else {
1093
1098
  if (dmOwner.kind === "ok") {
1094
1099
  if (dmOwner.owner.pid === undefined || !alive(dmOwner.owner.pid)) return { ok: false, reason: "dm-owner-dead" };
1095
- if (dmOwner.owner.sessionFile === undefined || !sessionWithinScan(dmOwner.owner.sessionFile, scan.dirs)) {
1100
+ if (
1101
+ scan !== undefined &&
1102
+ (dmOwner.owner.sessionFile === undefined || !sessionWithinScan(dmOwner.owner.sessionFile, scan.dirs))
1103
+ ) {
1096
1104
  return { ok: false, reason: "dm-owner-unrelated" };
1097
1105
  }
1098
1106
  recipient = "dm-owner";
@@ -1102,7 +1110,10 @@ export function telegramPlumbingVerdict(
1102
1110
  // record must name that session. A daemon-only or unrelated owner is no
1103
1111
  // recipient: the reply would land in a transcript arm never scans.
1104
1112
  if (poll.kind !== "ok") return { ok: false, reason: "no-dm-owner" };
1105
- if (poll.owner.sessionFile === undefined || !sessionWithinScan(poll.owner.sessionFile, scan.dirs)) {
1113
+ if (
1114
+ scan !== undefined &&
1115
+ (poll.owner.sessionFile === undefined || !sessionWithinScan(poll.owner.sessionFile, scan.dirs))
1116
+ ) {
1106
1117
  return { ok: false, reason: "no-dm-owner" };
1107
1118
  }
1108
1119
  recipient = "poller";
@@ -103,6 +103,23 @@ export function infraLogSignature(log: string): string | undefined {
103
103
  return INFRA_LOG_SIGNATURES.find((signature) => lower.includes(signature));
104
104
  }
105
105
 
106
+ /**
107
+ * The cap kills that produced nothing: the worker reached a ceiling with no PR,
108
+ * no observed head and no salvage commit.
109
+ *
110
+ * Named here — beside the table that assigns them — because two callers need
111
+ * exactly the same set and neither may guess at it: the escalate recovery
112
+ * quotes the transcript's last tool calls for these classes, and the one-shot
113
+ * model escalation (#807) fires only for these. Their `-progress` siblings are
114
+ * deliberately absent: a cap kill *with* work to continue from is a
115
+ * decomposition verdict, and retrying it on a stronger tier would hide an
116
+ * oversized slice behind a capability problem.
117
+ */
118
+ export const SPINNING_CAP_CLASSES: readonly FailureClass[] = [
119
+ "turn-cap-spinning",
120
+ "wall-clock-cap-spinning",
121
+ ];
122
+
106
123
  /**
107
124
  * A stable fingerprint of the infrastructure signature list (#638). The
108
125
  * historical reconciliation persists a per-project review cursor stamped with
@@ -315,6 +332,48 @@ export function dispatchInfra(
315
332
  // front of the thrown message, so a bare /^git / would never match a real row.
316
333
  const DISPATCH_GIT_ERROR = /^(?:Error: )?git .+ exited \d+/s;
317
334
 
335
+ /**
336
+ * Evidence that one review round's resumed session never got as far as a turn,
337
+ * or `undefined` when the round actually ran (#903).
338
+ *
339
+ * The measurement is the round's OWN session, never the run row's cumulative
340
+ * counters: a revision reuses its run, so `run.turns` already carries every
341
+ * turn the original attempt spent and would never read zero.
342
+ *
343
+ * Deliberately wider than {@link neverStarted}: any zero-turn round that
344
+ * produced nothing counts, whatever the error text says. The reference
345
+ * incident is why. During the 2026-08-21/22 worker-UID outage every revision
346
+ * dispatch died at turn 0 on `EACCES .../config.yml` — an error string no
347
+ * signature list contained — and each death still consumed one of the PR's
348
+ * three review rounds, until the ceiling refused any further round and the PR
349
+ * could not be finished at all. The discriminator that matters is "did the
350
+ * resumed session take a turn", not which errno the host produced; matching on
351
+ * errno text is how the next outage spells its failure differently and lands
352
+ * back in the same deadlock.
353
+ *
354
+ * Two things keep the widening honest, and both are the caller's:
355
+ * - it decides only whether a round is CHARGED, never whether a run is
356
+ * classified — run-level classification is untouched, so an unrecognised
357
+ * turn-0 run still reads `unknown` and still spends its attempt;
358
+ * - a retried round is bounded by {@link REVIEW_ROUND_INFRA_MAX_RETRIES}, so
359
+ * a permanently broken host escalates instead of retrying forever.
360
+ *
361
+ * A round that produced a PR, a head or a salvage did work no matter what its
362
+ * turn counter says, and is never this.
363
+ */
364
+ export function reviewRoundNeverWorked(round: {
365
+ turns: number;
366
+ prUrl?: string;
367
+ headSha?: string;
368
+ salvageSha?: string;
369
+ }): string | undefined {
370
+ if (round.turns !== 0) return undefined;
371
+ if (round.prUrl !== undefined || round.headSha !== undefined || round.salvageSha !== undefined) {
372
+ return undefined;
373
+ }
374
+ return "the resumed session never took a turn: no turn, no commit and no pushed head";
375
+ }
376
+
318
377
  /** The prefix the draining/restarting process writes to a run it killed. */
319
378
  const ADMIN_RESTART_MARKER = "admin restart:";
320
379