omp-conductor 0.18.2 → 0.19.1

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 (60) hide show
  1. package/README.md +105 -40
  2. package/REFERENCE.md +865 -30
  3. package/package.json +1 -1
  4. package/schema/config.schema.json +26 -0
  5. package/src/admission.ts +212 -26
  6. package/src/ask.ts +288 -1
  7. package/src/briefs/orchestrator.md +6 -5
  8. package/src/cli.ts +5 -1
  9. package/src/command-help.ts +9 -1
  10. package/src/command-manifest.ts +36 -3
  11. package/src/commands/arm.ts +5 -1
  12. package/src/commands/context.ts +2 -0
  13. package/src/commands/message.ts +26 -2
  14. package/src/commands/reconcile-units.ts +104 -0
  15. package/src/commands/release-composition.ts +232 -0
  16. package/src/commands/resume.ts +2 -27
  17. package/src/commands/setup.ts +101 -16
  18. package/src/commands/stats.ts +11 -30
  19. package/src/commands/tail.ts +31 -1
  20. package/src/commands/upgrade.ts +20 -3
  21. package/src/commands/verb.ts +2 -1
  22. package/src/config-schema.ts +19 -0
  23. package/src/config.ts +80 -0
  24. package/src/credential-class.ts +366 -0
  25. package/src/daemon.ts +1218 -288
  26. package/src/dashboard/app.js +504 -2
  27. package/src/dashboard/controls.ts +336 -0
  28. package/src/dashboard/index.html +30 -0
  29. package/src/dashboard/server.ts +271 -30
  30. package/src/dashboard/style.css +116 -0
  31. package/src/dashboard/transcript.ts +173 -0
  32. package/src/doctor.ts +379 -22
  33. package/src/failure-class.ts +59 -0
  34. package/src/fleet.ts +511 -101
  35. package/src/host.ts +6 -130
  36. package/src/omp.ts +29 -0
  37. package/src/orchestrator-tick.ts +343 -88
  38. package/src/pause.ts +233 -0
  39. package/src/settlement.ts +159 -2
  40. package/src/setup-answers.ts +97 -0
  41. package/src/setup-host.ts +325 -1159
  42. package/src/setup-install.ts +204 -27
  43. package/src/setup-wizard.ts +111 -50
  44. package/src/setup.ts +33 -0
  45. package/src/spend-telemetry.ts +117 -0
  46. package/src/stats.ts +35 -0
  47. package/src/status-render.ts +348 -19
  48. package/src/store.ts +1229 -55
  49. package/src/telegram-freshness.ts +269 -0
  50. package/src/to-spec.ts +27 -0
  51. package/src/types.ts +697 -4
  52. package/src/unblock.ts +22 -0
  53. package/src/unit-reconcile.ts +303 -0
  54. package/src/upgrade-verify.ts +8 -1
  55. package/src/upgrade.ts +326 -47
  56. package/src/verbs/actions.ts +124 -10
  57. package/src/verbs/protocol.ts +70 -2
  58. package/src/verbs/server.ts +447 -8
  59. package/src/wake.ts +48 -0
  60. package/src/worker.ts +403 -3
package/README.md CHANGED
@@ -271,39 +271,39 @@ The Herdr half owns recovery, not dispatch or policy: it restores the exact
271
271
  session identity, requests an immediate heartbeat, or reports through Telegram
272
272
  and a Herdr notification that the fleet is down.
273
273
 
274
- ### The worker identity and its harness binding
275
-
276
- Worker sessions do not run as you. `setup host` creates a dedicated
277
- unprivileged account, `omp-worker` (home `/var/lib/omp-worker`), and the daemon
278
- launches every worker session under it through `setpriv`. The account is granted
279
- search access to the paths a session needs and read access to your agent config
280
- files; its writable world is the worktree and session directory dispatch hands
281
- it per run, and nothing else.
282
-
283
- That boundary means the account cannot list your home which is also where
284
- `omp-conductor` and its `@oh-my-pi/pi-coding-agent` peer are installed, and
285
- module resolution needs to list a directory to find the `node_modules` inside
286
- it. Left there, a worker silently resolved a *different* harness version out of
287
- its own package cache. So `setup host` also installs one mount unit,
288
- `var-lib-omp\x2dworker\x2dharness-node_modules.mount`, binding your install
289
- read-only at `/var/lib/omp-worker-harness/node_modules`, and launches worker
290
- children from that path. It is a bind, not a copy: upgrade the harness and every
291
- worker picks it up with nothing to re-materialise. The mount's parent directory
292
- is `root:omp-worker` `0750`, so the bound tree is reachable by root and the
293
- worker account and by no other local account.
294
-
295
- Three consequences worth knowing:
296
-
297
- - `omp-conductor` must be installed, not run from a source checkout, for worker
298
- dispatch to work a checkout has no install root to bind.
299
- - If the binding is missing or no longer resolves to your install, the daemon
300
- refuses to launch workers and says so (`setup host` reports the same thing as
301
- pending work). Re-run `omp-conductor setup host`; no attempt is charged to the
302
- issue, because no session ever started.
303
- - The check runs at every launch, not once at daemon startup. The mount unit is
304
- ordered before the daemon, so a reboot brings them up in the right order — and
305
- if the binding ever arrives late anyway, the next dispatch picks it up with no
306
- restart needed.
274
+ ### What confines a worker session
275
+
276
+ Worker sessions run as the fleet account the same account the daemon runs as.
277
+ An earlier release ran them under a dedicated `omp-worker` UID with your install
278
+ bind-mounted read-only for them to resolve; that design did not survive contact
279
+ with Bun's install, cache and native-addon resolution (a worker could not load
280
+ the harness at all), and it was withdrawn. Exposing package files is not a
281
+ self-contained runnable runtime.
282
+
283
+ What confines a worker is unchanged, and none of it needed a separate account:
284
+
285
+ - **its worktree.** Dispatch hands each run an isolated git worktree and session
286
+ directory; that is its writable world for the attempt.
287
+ - **its verbs.** A worker session may push and open a pull request through the
288
+ mediated verbs and nothing else: `conductor_pr_merge`, `conductor_label` and
289
+ `conductor_release` refuse a worker mechanically, whatever any config says.
290
+ - **its budget.** Turn, wall-clock, spend and attempt caps bound every run, and
291
+ a cap kill is recorded rather than retried silently.
292
+ - **its review.** A green pull request is where a worker stops; merging is the
293
+ orchestrator's, behind the exact-head, green-checks and single-flight gates.
294
+
295
+ `setup host` no longer provisions that account, its ACL projection or the mount
296
+ unit, and a host that carries them from an earlier release is converged the next
297
+ time `setup host` or `upgrade` runs: the mount is disabled, unmounted and
298
+ removed, before anything else in the transaction. The account itself is left
299
+ alone an unused Unix account is inert, and deleting one to tidy up is
300
+ destructive.
301
+
302
+ OS-user isolation is a design worth revisiting, and the next attempt needs an
303
+ immutable self-contained worker runtime rather than a bind of the operator's
304
+ install, a real end-to-end session preflight before any switch, and atomic
305
+ rollback. Until then, `omp-conductor` still has to be *installed* (not run from a
306
+ source checkout) for a worker to resolve its harness peer.
307
307
 
308
308
  ### Stop the conductor (hold / stop)
309
309
 
@@ -407,12 +407,77 @@ Clear the pin with `omp-conductor resume` when you want recovery again.
407
407
  ### See the fleet in a browser: `dashboard`
408
408
 
409
409
  `omp-conductor dashboard` serves the same fleet facts `status` and `board`
410
- render, in a browser: a static UI plus one read endpoint, `GET /api/projects`,
411
- which answers with every configured project annotated with its daemon state
412
- (the same `livingDaemon` + `/healthz` classification `status` uses), the
413
- daemon's port, and the raw `/healthz` body when it answers. It is a separate
414
- process from the dispatch daemon and only ever reads fleet state nothing on
415
- the daemon port changes.
410
+ render, in a browser, and since #295 lets you *act* on them without a shell
411
+ on the host. It is a separate process from the dispatch daemon: the read surface
412
+ (`GET /api/projects`, `/api/overview`, and per-project `board`, `status`,
413
+ `ledger`, `reports`, `runs/:issue`) reads fleet state exactly as `status` does,
414
+ and the control surface (`POST`) routes every action to the same code the CLI
415
+ verb runs.
416
+
417
+ | action | what it does |
418
+ |---|---|
419
+ | `pause` / `resume` | stop and restart new claims, **keeping the heartbeat** — the cheap, reversible control |
420
+ | `hold` | stop claims *and* disarm ticks; re-arming costs a Telegram challenge, so the UI confirms it by name |
421
+ | `disarm` | take the heartbeat down without touching dispatch |
422
+ | `runs/:issue/extend` | raise a run's turn ceiling, **proxied to the owning daemon** so its own bounds and refusals apply verbatim |
423
+ | `runs/:issue/worker/{pause,resume,stop}` | drive one live worker, proxied for the same reason — the daemon holds the session controller |
424
+ | `runs/:issue/unblock` | the same `unblock` the CLI runs, `force` included and confirmed by name |
425
+ | `decisions/:id/answer` | resolve one open decision; a second answer is a `409`, never a silent overwrite |
426
+
427
+ Three properties are worth knowing before you use it:
428
+
429
+ - **Nothing here is a second implementation.** Where an action already lives
430
+ behind the daemon's HTTP surface, the dashboard *calls that endpoint* rather
431
+ than reaching past it to the store. `extend` is the instructive case: the
432
+ daemon's `turn-limit` route owns the ceiling bound, the live-controller check
433
+ and the not-an-increase refusal, so its `409`/`422` answers pass through
434
+ unchanged and the browser cannot come to disagree with `omp-conductor extend`.
435
+ - **A dead daemon is a `502` naming it**, never a `200` that pretends a worker
436
+ was paused.
437
+ - **`arm` is deliberately absent.** It requires an inbound Telegram proof that a
438
+ human answered a challenge, and a browser button cannot supply one — a
439
+ dashboard `arm` would either weaken that proof or lie about having it. Arming
440
+ stays CLI/Telegram.
441
+
442
+ **Watching a run.** The run detail view streams the transcript live
443
+ (`GET .../runs/:issue/transcript/stream`, Server-Sent Events). It is the same
444
+ follow `omp-conductor tail` runs — the same 1s re-stat cadence, reading from byte
445
+ zero so attaching to a run ten turns in shows all ten, and the same advisor
446
+ transcripts alongside. Nothing here touches the daemon: two browsers on one run
447
+ are two read-only file follows, and a fleet with no daemon running at all still
448
+ streams. A run with no transcript yet gets one explicit `error` event saying so
449
+ rather than an empty stream that reads as a quiet worker; a finished run streams
450
+ its tail, writes `run ended: <state>`, and closes.
451
+
452
+ One wrinkle worth knowing: `EventSource` cannot set an `Authorization` header, so
453
+ this endpoint — and only this endpoint — also accepts `?token=`. A URL can reach
454
+ a server log or a browser history entry, which is why the acceptance is scoped to
455
+ that exact path and stated rather than buried.
456
+
457
+ **Analytics.** The Analytics tab answers *what did the fleet accomplish, and at
458
+ what cost* — merges per week, USD per merged PR, lead-time p50/p90, attempts per
459
+ merge, and a failure-class histogram, over a 7d/30d/custom window. It is the
460
+ same aggregation `omp-conductor stats` prints, computed by the same module: one
461
+ implementation, two consumers, so the page and the CLI cannot disagree about a
462
+ number. Store-only — rendering it makes no GitHub API calls.
463
+
464
+ Two things it deliberately will not do. A window in which nothing settled
465
+ renders an explicit *nothing settled* rather than a chart of zeros, because a
466
+ zero drawn on an axis reads as a measurement. And runs whose harness reported no
467
+ spend (`spendUsd = 0`) are counted as **unmetered**, never averaged in as free:
468
+ cost-per-merge divides by the merged issues that actually metered, and the
469
+ unmetered remainder is named beside it.
470
+
471
+ **Answering a question.** Open decisions render on the Reports tab with an
472
+ answer box, resolving through the same `PUT /decisions/:id/answer` the CLI's
473
+ `decision resolve` uses — so a second answer to the same question is a 409, not
474
+ a silent overwrite. Watches are listed but never offered an answer box: a watch
475
+ is the orchestrator's own condition, not a question anyone asked you.
476
+
477
+ Every mutation attributes itself with source `dashboard`, so `status` can say
478
+ *who* paused the fleet rather than only that it is paused. The operator's own
479
+ sentence rides in the sentinel's `reason=` field, because `source=` is a
480
+ space-free token the pause fence matches on.
416
481
 
417
482
  Binds `127.0.0.1:8788` by default. First start mints a bearer token at
418
483
  `<stateDir()>/dashboard-token` (mode `0600`, next to `config.json` under
@@ -620,7 +685,7 @@ reference — usage strings, flags and per-command behaviour — is in the
620
685
  | `doctor [--json] [--probe-telegram]` | Read-only deployment health — gh auth, exact-case labels, systemd drift, config backup, sqlite integrity, spend telemetry, timezones, Telegram. Run after install and after every upgrade; exit 0 only when nothing failed. |
621
686
  | `ledger [--json]` | The action audit — every mediated verb, refusal and turn budget. |
622
687
  | `board` | Live terminal kanban from Queue to Settled. |
623
- | `dashboard [--port N] [--host ADDR]` | Browser UI plus bearer-authenticated `/api/projects`: every project's daemon state, port and healthz. Loopback by default; the token lives at `<state>/dashboard-token`. |
688
+ | `dashboard [--port N] [--host ADDR]` | Browser UI plus a bearer-authenticated API: read the fleet (`/api/projects`, `overview`, `board`, `status`, `ledger`, `reports`, `runs`, `stats`) and drive it (`pause`/`resume`/`hold`/`disarm`, `extend`, `unblock`, worker control, decision answering), plus a live SSE transcript stream per run and an analytics view over the same aggregation `stats` prints. `arm` is deliberately absent. Loopback by default; the token lives at `<state>/dashboard-token`. |
624
689
  | `hold [--keep-ticks]` | Pause claims and disarm ticks — the soft stop. |
625
690
  | `arm` / `disarm` | Gate / clear the orchestrator tick's arm marker. |
626
691
  | `tail <issue>` | Follow a live worker's transcript. |