@usine/orchestrator 0.2.1 → 0.3.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 (173) hide show
  1. package/README.md +151 -52
  2. package/dist/board/items.d.ts +9 -1
  3. package/dist/board/items.d.ts.map +1 -1
  4. package/dist/board/items.js +16 -4
  5. package/dist/board/items.js.map +1 -1
  6. package/dist/board/projection.d.ts +7 -1
  7. package/dist/board/projection.d.ts.map +1 -1
  8. package/dist/board/projection.js +53 -14
  9. package/dist/board/projection.js.map +1 -1
  10. package/dist/board/tasks.d.ts +14 -2
  11. package/dist/board/tasks.d.ts.map +1 -1
  12. package/dist/board/tasks.js +23 -8
  13. package/dist/board/tasks.js.map +1 -1
  14. package/dist/credentials.d.ts +5 -6
  15. package/dist/credentials.d.ts.map +1 -1
  16. package/dist/credentials.js +22 -15
  17. package/dist/credentials.js.map +1 -1
  18. package/dist/current-instance/changes.d.ts +87 -0
  19. package/dist/current-instance/changes.d.ts.map +1 -0
  20. package/dist/current-instance/changes.js +138 -0
  21. package/dist/current-instance/changes.js.map +1 -0
  22. package/dist/current-instance/current-instance.d.ts +89 -0
  23. package/dist/current-instance/current-instance.d.ts.map +1 -0
  24. package/dist/current-instance/current-instance.js +163 -0
  25. package/dist/current-instance/current-instance.js.map +1 -0
  26. package/dist/exe-dev.d.ts +6 -2
  27. package/dist/exe-dev.d.ts.map +1 -1
  28. package/dist/exe-dev.js +7 -8
  29. package/dist/exe-dev.js.map +1 -1
  30. package/dist/idle-triggers.d.ts +1 -1
  31. package/dist/idle-triggers.d.ts.map +1 -1
  32. package/dist/idle-triggers.js +1 -1
  33. package/dist/idle-triggers.js.map +1 -1
  34. package/dist/label-reconciler.d.ts +5 -2
  35. package/dist/label-reconciler.d.ts.map +1 -1
  36. package/dist/label-reconciler.js.map +1 -1
  37. package/dist/local.d.ts +4 -18
  38. package/dist/local.d.ts.map +1 -1
  39. package/dist/local.js +7 -45
  40. package/dist/local.js.map +1 -1
  41. package/dist/loop-matches.d.ts +28 -0
  42. package/dist/loop-matches.d.ts.map +1 -0
  43. package/dist/loop-matches.js +74 -0
  44. package/dist/loop-matches.js.map +1 -0
  45. package/dist/operator/attempt.d.ts +57 -10
  46. package/dist/operator/attempt.d.ts.map +1 -1
  47. package/dist/operator/attempt.js +49 -10
  48. package/dist/operator/attempt.js.map +1 -1
  49. package/dist/operator/board.d.ts +43 -16
  50. package/dist/operator/board.d.ts.map +1 -1
  51. package/dist/operator/board.js +43 -16
  52. package/dist/operator/board.js.map +1 -1
  53. package/dist/operator/configuration.d.ts +258 -0
  54. package/dist/operator/configuration.d.ts.map +1 -0
  55. package/dist/operator/configuration.js +410 -0
  56. package/dist/operator/configuration.js.map +1 -0
  57. package/dist/operator/desk.d.ts +181 -0
  58. package/dist/operator/desk.d.ts.map +1 -0
  59. package/dist/operator/desk.js +463 -0
  60. package/dist/operator/desk.js.map +1 -0
  61. package/dist/operator/github-changes.d.ts +88 -0
  62. package/dist/operator/github-changes.d.ts.map +1 -0
  63. package/dist/operator/github-changes.js +266 -0
  64. package/dist/operator/github-changes.js.map +1 -0
  65. package/dist/operator/http.d.ts +152 -57
  66. package/dist/operator/http.d.ts.map +1 -1
  67. package/dist/operator/http.js +140 -21
  68. package/dist/operator/http.js.map +1 -1
  69. package/dist/operator/need.d.ts +44 -11
  70. package/dist/operator/need.d.ts.map +1 -1
  71. package/dist/operator/need.js +46 -9
  72. package/dist/operator/need.js.map +1 -1
  73. package/dist/operator/operator.d.ts +109 -29
  74. package/dist/operator/operator.d.ts.map +1 -1
  75. package/dist/operator/operator.js +254 -52
  76. package/dist/operator/operator.js.map +1 -1
  77. package/dist/operator/overview.d.ts +7 -7
  78. package/dist/operator/overview.d.ts.map +1 -1
  79. package/dist/operator/overview.js +5 -5
  80. package/dist/operator/overview.js.map +1 -1
  81. package/dist/operator/projection.d.ts +54 -5
  82. package/dist/operator/projection.d.ts.map +1 -1
  83. package/dist/operator/projection.js +100 -9
  84. package/dist/operator/projection.js.map +1 -1
  85. package/dist/operator/run.d.ts +71 -6
  86. package/dist/operator/run.d.ts.map +1 -1
  87. package/dist/operator/run.js +69 -6
  88. package/dist/operator/run.js.map +1 -1
  89. package/dist/operator/task.d.ts +93 -0
  90. package/dist/operator/task.d.ts.map +1 -0
  91. package/dist/operator/task.js +204 -0
  92. package/dist/operator/task.js.map +1 -0
  93. package/dist/operator/transcript.d.ts +12 -12
  94. package/dist/operator/transcript.d.ts.map +1 -1
  95. package/dist/operator/transcript.js +11 -9
  96. package/dist/operator/transcript.js.map +1 -1
  97. package/dist/operator/usage.d.ts +111 -0
  98. package/dist/operator/usage.d.ts.map +1 -0
  99. package/dist/operator/usage.js +219 -0
  100. package/dist/operator/usage.js.map +1 -0
  101. package/dist/orchestrator.d.ts +8 -5
  102. package/dist/orchestrator.d.ts.map +1 -1
  103. package/dist/orchestrator.js +4 -2
  104. package/dist/orchestrator.js.map +1 -1
  105. package/dist/polling/cycle.d.ts +28 -10
  106. package/dist/polling/cycle.d.ts.map +1 -1
  107. package/dist/polling/cycle.js +20 -6
  108. package/dist/polling/cycle.js.map +1 -1
  109. package/dist/polling/evaluation-failures.d.ts +42 -0
  110. package/dist/polling/evaluation-failures.d.ts.map +1 -0
  111. package/dist/polling/evaluation-failures.js +103 -0
  112. package/dist/polling/evaluation-failures.js.map +1 -0
  113. package/dist/polling/hints.d.ts +2 -2
  114. package/dist/polling/hints.d.ts.map +1 -1
  115. package/dist/polling/hints.js +2 -2
  116. package/dist/polling/hints.js.map +1 -1
  117. package/dist/polling/loop-evaluation.d.ts +21 -11
  118. package/dist/polling/loop-evaluation.d.ts.map +1 -1
  119. package/dist/polling/loop-evaluation.js +70 -27
  120. package/dist/polling/loop-evaluation.js.map +1 -1
  121. package/dist/polling/open-issues.d.ts +23 -0
  122. package/dist/polling/open-issues.d.ts.map +1 -0
  123. package/dist/polling/open-issues.js +77 -0
  124. package/dist/polling/open-issues.js.map +1 -0
  125. package/dist/polling/polling.d.ts +20 -11
  126. package/dist/polling/polling.d.ts.map +1 -1
  127. package/dist/polling/polling.js +201 -51
  128. package/dist/polling/polling.js.map +1 -1
  129. package/dist/polling/repository-sync.d.ts +20 -14
  130. package/dist/polling/repository-sync.d.ts.map +1 -1
  131. package/dist/polling/repository-sync.js +34 -27
  132. package/dist/polling/repository-sync.js.map +1 -1
  133. package/dist/polling-triggers.d.ts +1 -1
  134. package/dist/polling-triggers.d.ts.map +1 -1
  135. package/dist/polling-triggers.js +1 -1
  136. package/dist/polling-triggers.js.map +1 -1
  137. package/dist/reloads.d.ts +14 -0
  138. package/dist/reloads.d.ts.map +1 -0
  139. package/dist/reloads.js +7 -0
  140. package/dist/reloads.js.map +1 -0
  141. package/dist/runtime.d.ts +24 -0
  142. package/dist/runtime.d.ts.map +1 -0
  143. package/dist/runtime.js +58 -0
  144. package/dist/runtime.js.map +1 -0
  145. package/dist/schedule-triggers.d.ts +14 -10
  146. package/dist/schedule-triggers.d.ts.map +1 -1
  147. package/dist/schedule-triggers.js +36 -31
  148. package/dist/schedule-triggers.js.map +1 -1
  149. package/dist/scheduler.d.ts +35 -4
  150. package/dist/scheduler.d.ts.map +1 -1
  151. package/dist/scheduler.js +327 -41
  152. package/dist/scheduler.js.map +1 -1
  153. package/dist/ssh.d.ts +39 -0
  154. package/dist/ssh.d.ts.map +1 -0
  155. package/dist/ssh.js +75 -0
  156. package/dist/ssh.js.map +1 -0
  157. package/dist/trigger-condition.d.ts +12 -0
  158. package/dist/trigger-condition.d.ts.map +1 -0
  159. package/dist/trigger-condition.js +88 -0
  160. package/dist/trigger-condition.js.map +1 -0
  161. package/dist/trigger-submission.d.ts +16 -13
  162. package/dist/trigger-submission.d.ts.map +1 -1
  163. package/dist/trigger-submission.js +19 -14
  164. package/dist/trigger-submission.js.map +1 -1
  165. package/dist/worker-engine.d.ts +10 -0
  166. package/dist/worker-engine.d.ts.map +1 -0
  167. package/dist/worker-engine.js +25 -0
  168. package/dist/worker-engine.js.map +1 -0
  169. package/dist/worker-status.d.ts +70 -0
  170. package/dist/worker-status.d.ts.map +1 -0
  171. package/dist/worker-status.js +55 -0
  172. package/dist/worker-status.js.map +1 -0
  173. package/package.json +31 -10
package/README.md CHANGED
@@ -1,49 +1,90 @@
1
1
  # @usine/orchestrator
2
2
 
3
- `@usine/orchestrator` connects loaded loop files to GitHub polling, the durable run engine, and configured workers for [usine](https://github.com/basaltbytes/usine).
3
+ Connects loaded loop files to GitHub polling, the durable run engine, and the configured workers for [usine](https://github.com/basaltbytes/usine). [Operator surfaces](https://docs.usine.dev/architecture/operator-surfaces/) and [Run lifecycle](https://docs.usine.dev/architecture/run-lifecycle/) describe the behavior in depth.
4
4
 
5
- Its platform-free entry points provide `PollingTriggers` at `/polling-triggers`, `ScheduleTriggers` at `/schedule-triggers`, `Orchestrator` at `/orchestrator`, and `Operator` with its request and response types at `/operator`. `Orchestrator.run(instance)` runs both trigger streams; `Operator` provides run and Board observation, structured protocol transcript pages, label-backed Board moves, approve/retry/cancel, and database-backed Loop pause/unpause. Transcript payloads, Attempt brains/outcomes, and items needing attention remain typed data. Captured executor defects appear as `DefectFailure` with a safe message; original causes stay out of operator responses. Consuming apps own their wording and presentation. `OperatorObservation` exposes the same read-only projection without constructing execution services. The latest polling and schedule failures remain visible through `Operator.overview(instance)` after their sibling work continues. Concurrent overview reads with the same durable revision share one projection computation.
5
+ The platform-free entry points are `PollingTriggers` (`/polling-triggers`), `ScheduleTriggers` (`/schedule-triggers`), `Orchestrator` (`/orchestrator`), and `Operator` with its request and response types (`/operator`). `Orchestrator.run` runs both trigger streams over the factory's `CurrentInstance`.
6
6
 
7
- `Operator.overview(instance)` joins the latest 500 polled Items into Task cards in each repository's authored lanes. Explicit same-repository issue closing links and saved Run PR results join an issue to a PR. Repositories without configured lanes receive proposals from the repository label catalog, including unused labels and repositories with no Items. Items that match no source appear in a read-only `Unmapped` lane, and label matches take precedence over state sources. Each card retains every saved Run on its member Items, oldest first. The newest open Run determines the current Item; between Runs an open PR, then the latest settled PR, takes over from the issue. Its labels and state determine the lane, so removing the issue's trigger label does not strand a task whose PR is under review. See ADR 0024. A malformed Item projection is quarantined and reported in the Board and needs-you strip while valid siblings remain visible. Failed declared-label creation also appears as a repository preflight finding without blocking trigger work.
7
+ 0.x APIs can change between minor releases. Supply `effect` at the workspace's pinned version.
8
8
 
9
- `Operator.moveBoardItem(instance, move)` accepts authored label-backed lanes, adds the target lane labels, and removes only labels owned by the authored map. Proposed lanes are read-only; direct move requests return `BoardLaneReadOnlyError`. GitHub labels outside the authored map remain unchanged even when the Item snapshot is stale or its label connection is truncated. Configure `repos[].lanes` in `usine.yaml` and restart the orchestrator to enable moves; see the [lane configuration example](../config/README.md). Every attempted GitHub label write queues an Item Hint, including an uncertain or partially applied failure: the running poller fetches the current GitHub snapshot, projects its Board position, and evaluates its Loops immediately unless GitHub has requested backoff.
9
+ ## Operator
10
10
 
11
- `Operator.detail` accepts distinct cold, live-tail, and earlier-page requests. A cold read keeps the newest 500 rows per Attempt and returns an earlier cursor for every truncated Attempt; clients can page each cursor backward until the entire transcript is reachable without ballooning the first drawer response. A live-tail response returns at most 500 rows across the Run and advances its cursor from the last returned row.
11
+ `Operator` provides run and board observation, transcript pages, label-backed board moves, approve, retry, cancel, and loop pause. It returns typed data, and consuming apps own the wording. Captured executor defects appear as `DefectFailure` with a safe message, without the original cause. `OperatorObservation` gives the same read-only projection without execution services.
12
+
13
+ `OperatorConfiguration.view(instance)` returns the instance folder as already parsed: each loop, agent, worker, registered repo with its lanes, and the settings; every quarantined loop and invalid agent with its error; each loop's pause; each worker's slots and last preflight per harness (from `WorkerStatus`); and each repo's labels as polling last read them. A gateway's admin token appears only as its variable name.
14
+
15
+ `Operator.overview(instance)` keeps the latest polling and schedule failures visible after sibling work continues. Concurrent reads at one durable revision share a computation.
16
+
17
+ ### Overview and board
18
+
19
+ The overview joins the latest 500 polled items into task cards in each repo's authored lanes. Same-repo issue-closing links and saved run PR results join an issue to a PR. Repos without lanes get proposals from the label catalog, including unused labels and repos without items. Items matching no source go to a read-only `Unmapped` lane; label matches beat state sources. Each card keeps every saved run on its items, oldest first.
20
+
21
+ The newest open run sets the current item; between runs, an open PR, then the latest settled PR, replaces the issue. Its labels and state pick the lane, so a task whose PR is in review stays put when the issue loses its trigger label. While the newest open run is `Running` on an open item, the card goes to the first label lane matching the labels in that run's event payload (`RunReader.events`), until the run ends; if none matches, the current item decides. `Operator.task` places tasks the same way, and `Operator.moveBoardItem` records the lane the card left (ADR 0024).
22
+
23
+ A card also has `pullRequest` (the open PR, else the newest merged one) and `usage` from `RunUsageReader`: ended attempt time, working attempts' starts, and tokens and dollars per billing pool over its runs' sessions. A pool's dollars are its priced sessions'; `unpriced` marks a session in a priced pool without a price yet, such as a Claude session before its first result, so the cost is a lower bound. The overview is kept until the revision or instance changes; token reports change neither, so each read re-reads working attempts' reports and re-sums cards whose runs used more.
24
+
25
+ A malformed item projection is quarantined and reported on the board and the needs-attention strip; valid items stay visible. A failed declared-label creation is a repo preflight finding and doesn't block triggers.
26
+
27
+ `Operator.moveBoardItem(instance, move)` accepts authored label-backed lanes, adds the target lane's labels, and removes only labels the authored map owns, even with a stale snapshot or truncated label connection. Proposed lanes are read-only (`BoardLaneReadOnlyError`); define [`repos[].lanes`](https://docs.usine.dev/reference/configuration/usine-yaml/) and reload to enable moves. Every attempted label write, including an uncertain or partial failure, queues an item hint: the poller fetches the snapshot, places it, and evaluates its loops at once unless GitHub requested backoff.
28
+
29
+ ### Run detail
30
+
31
+ `Operator.detail` takes distinct cold, live-tail, and earlier-page requests. A cold read keeps the newest 500 rows per attempt, with an earlier cursor per truncated attempt for paging back. A live tail returns at most 500 rows across the run and advances its cursor from the last row. Each response also has:
32
+
33
+ - the newest attempt's worker;
34
+ - `RunDurationView`: ended attempts' time, the running one's start, and how many ended without a recorded end or transcript row;
35
+ - `RunUsageView`: each session's highest token report, summed per session, pool, and run, priced where the pool has a dollar rate, as an estimate on the Claude plan and never on the ChatGPT plan (ADR 0013). Pools come from `sessionPool` (`@usine/core/agent/session`), so a Claude Code child on its gateway's API key counts in API keys at the model's rate. Ended attempts' reports are kept in memory until restart, so polling reads only the attempt in flight.
36
+
37
+ Each `AttemptView` has `start`, the last reattach time, and `workerAccess`: `LocalWorkerAccess`, `SshWorkerAccess` with `destination` and `port` from `usine.yaml` (exe.dev reads as `<name>.exe.xyz`), or null if the worker is no longer declared. `detail`, like the overview, task, and desk reads, takes the loaded instance. `Operator.retry` and `Operator.editAndRetry` pass the instance's `operator` login to the next attempt.
38
+
39
+ ### Task
40
+
41
+ `Operator.task(instance, repository, item)` returns the `TaskView` containing `item`, found from any member and named by its originating item (the earliest issue, else the earliest PR). It uses the board projection over the repo's 500 newest items, and `RunReader.forItems` for every saved run, outside the overview's window. It has the lane and current item; each item's state, labels, and base conflict; runs newest first; and the runs' comments and reviews, newest first per run: a succeeded attempt's `Comment` facts and a failed or canceled attempt's recorded comments, each with GitHub's post time (if recorded) and the attempt's end. An item outside the window fails with `BoardItemNotFoundError`. API: `GET /api/projects/:owner/:repository/tasks/:item`.
42
+
43
+ ### GitHub changes
44
+
45
+ `Operator.githubChanges(runId)` returns `RunGitHubChangesView`: per attempt, the labels added and removed, comments, and submitted reviews on the run's item, oldest first. It reads the timeline through `GitHubItemHistory`, keeps the App's changes, and gives each to the attempt at work when GitHub recorded it, from its start second to its end (or now). An attempt that ended before end times were recorded ends at its last transcript row. All agents write as one App, so a change during two runs' overlapping attempts belongs to neither and is logged.
46
+
47
+ Board moves use the same App, so `moveBoardItem` records each move in `BoardMoves` (`@usine/engine/board-moves`) before sending labels, and doesn't send a move it can't record. A timeline label change that the move sent the same way, recorded from 1 second before to 10 seconds after the move's second, belongs to the move: it leaves the attempt's list, and the move appears as `BoardMoved` with its lanes in the attempt at work then. A move with no label recorded, such as a refused write, doesn't appear. Moves from before recording existed still read as the agent's.
48
+
49
+ One read asks GitHub once, from the earliest uncached attempt's start. Attempts are cached in memory a minute after they end, so a finished run asks GitHub nothing until a restart. Schedule runs have no item and list no attempts. API: `GET /api/runs/:runId/github-changes`; a gone item returns 404 `GitHubItemNotFoundError`.
50
+
51
+ ### Cycle budgets, run again, and failure resolution
52
+
53
+ Cycle exhaustion is a `CycleBudgetNeed` in the overview and marks the card as needing attention. `Operator.resetCycleBudget` answers the exact ask, at `POST /api/cycle-budgets/reset` with `{ "askId": "…" }`; a stale or differently answered ask returns 409. Polling then restarts the repo cursor, fetches current items, and evaluates active loops before opening runs, retrying a failed refresh. Each item fetch captures its budget generation, resets supersede in-flight observations, and new events use the current loop configuration. GitHub delivery of these asks is [#36](https://github.com/basaltbytes/usine/issues/36).
54
+
55
+ `Operator.runAgain(instance, runId, edit)` opens a new run for a succeeded or canceled run's item state, with the edit's task and roster agent and the loaded loop's step, approval, and cycle limit, as the `operator` login. It returns `Claimed` or `CycleBlocked` with the ask, and fails with `LoopNotFoundError` if the loop isn't loaded. API: `POST /api/runs/:runId/run-again` with `{ "task": "...", "agent": "..." }`. `RunView.canRunAgain` says when it applies; `RunView.runAgain` names the original run.
56
+
57
+ `Operator.resolveFailure(runId, attemptId)` clears attention for the current failed attempt, at `POST /api/runs/:runId/resolve-failure` with `{ "attemptId": "..." }`. The failure stays in history with retry available; Needs attention, project status, and board attention reflect it, and a later failure asks again. For a question (`QuestionFailureView`) or blocker (`BlockedFailureView`), it lifts the loop's suppression of the item and gives the newest queued event its turn.
12
58
 
13
59
  ## Operator HTTP
14
60
 
15
- Import `FactoryOverview`, `RunView`, `RunDetailView`, Board views, operator needs,
16
- and transcript request types from `@usine/orchestrator/operator`. The `Operator`
17
- service and its HTTP contract use the same schemas for these views and requests.
18
-
19
- `@usine/orchestrator/operator/http` exports `OperatorApi`, the shared HTTP contract
20
- for operator observation and interventions. Use Effect's `HttpApiClient.make` with
21
- an `HttpClient` layer to call it, or `HttpApiBuilder.group` to implement its
22
- `operator` group. Server implementations provide `OperatorHttpPolicy` to enforce
23
- origin checks and translate request failures; `apps/server` provides the local
24
- server implementation.
25
-
26
- The generated client accepts the same `ColdTranscriptRequest`,
27
- `TailTranscriptRequest`, and `EarlierTranscriptRequest` values as `Operator.detail`.
28
- It encodes their exclusive query parameters and decodes responses through the
29
- shared read-model schemas. `OperatorHttpError` preserves the failure's `error`
30
- discriminator, operator-readable `message`, and associated `status`. The wire
31
- response remains `{ error, message }`. Successful actions return `{ ok: true }`;
32
- clients observe subsequent Run or Board progress through the read endpoints.
61
+ Import `FactoryOverview`, `RunView`, `RunDetailView`, board views, operator needs, and transcript request types from `/operator`. The `Operator` service and its HTTP contract share these schemas.
62
+
63
+ `/operator/http` exports `OperatorApi`, the HTTP contract for observation and interventions. Call it with Effect's `HttpApiClient.make` and an `HttpClient` layer, or implement its `operator` group with `HttpApiBuilder.group`. Servers provide `OperatorHttpPolicy` for origin checks and failure translation; `apps/server` is the local server.
64
+
65
+ The generated client takes `Operator.detail`'s `ColdTranscriptRequest`, `TailTranscriptRequest`, and `EarlierTranscriptRequest`, encodes their exclusive query parameters, and decodes with the shared schemas. `OperatorHttpError` keeps the failure's `error` discriminator, readable `message`, and `status`; the wire response is `{ error, message }`. Successful actions return `{ ok: true }`; clients follow progress through the read endpoints.
33
66
 
34
67
  ## Polling triggers
35
68
 
36
- `PollingTriggers.poll(instance)` performs one cycle; `PollingTriggers.run(instance)` repeats cycles using the instance's factory-wide `poll_interval` and waits longer when GitHub reports rate-limit pressure. Per-repository failures are isolated and reported on the cycle — one failing repository never stops its siblings. `PollingTriggers.hint(repository, item)` queues a bounded, coalesced self-write nudge. The running poller serializes Hint snapshots with regular cycles and uses the same projection and Event dedup path. Hints leave REST cursors, the normal conditional-poll cadence, and the last complete cycle’s findings intact; dropped or failed Hints are repaired by the polling backstop. GitHub request backoff, including deadlines reported by a failed writer, also delays Hint fetches. The original write outcome still reaches its caller.
69
+ `PollingTriggers.poll(instance)` runs one cycle. `PollingTriggers.run(instances)` repeats them every `poll_interval`, longer under rate-limit pressure, reading `instances.get` before each cycle and hint. A reload on `instances.changes` starts the next cycle at once, or when GitHub asked, if later. Repo failures are isolated and reported on the cycle.
70
+
71
+ `PollingTriggers.hint(repository, item)` queues a bounded, coalesced self-write nudge, serialized with cycles through the same projection and dedup path. Hints leave REST cursors, the poll cadence, and the last full cycle's findings unchanged, and polling repairs dropped or failed ones. GitHub backoff, including a failed writer's deadline, delays hints too. The write's own outcome still reaches its caller.
72
+
73
+ Each cycle first refreshes every repo's label catalog, which supplies lane colors, and creates labels declared in `usine.yaml` that are missing. It never touches existing labels, and logs color or description drift. Creation failures become preflight findings, and the cycle continues.
74
+
75
+ It then gets changed items from `GitHubPolling`, fetches one snapshot each, updates the projection, and evaluates each stream-matching loop's CEL `if:`. A match renders every `{{ CEL expression }}` in the loop body, resolves the agent and parent model, and stores that immutable context with the event, whose identity covers the loop, canonical repo, item number, and timestamp-free content hash. An unmatch revokes the loop's pending or approval-waiting work on the item. Repos without matching loops are still polled for the board.
37
76
 
38
- Each cycle first refreshes the complete label catalog in every managed repository and reconciles the factory-wide declarations from `usine.yaml`. Usine creates missing labels before change detection or Item evaluation. It leaves existing labels untouched and logs color or description drift. Creation failures become preflight findings, and the cycle continues into triggers. The updated catalog remains the Board's source for lane colors.
77
+ Change detection never lists deleted or transferred issues, so at most every 10 minutes per repo, the cycle checks every stored open issue with `GitHubPolling.missingIssues`. Missing ones are marked gone, leaving the open lanes, and their pending or approval-waiting work is revoked. A failed check is logged and reported, and retried 10 minutes later.
39
78
 
40
- After label preflight, the cycle asks `GitHubPolling` for changed items, fetches one payload snapshot per changed item, updates the durable Item projection, and evaluates each stream-matching loop's CEL `if:` expression against that snapshot. A match renders every `{{ CEL expression }}` in the loop body, resolves the named agent and effective parent model, then stores that immutable execution context with the Event. The Event identity covers the loop, canonical repository, item number, and timestamp-free content hash. An unmatch revokes pending or approval-waiting work for that loop and item. Repositories without matching loops are still polled for their Board projection.
79
+ An `if:` or prompt that fails on an item affects only that loop and item, and the cursor advances. The cursor records the failing item, so each cycle reports it until the loop evaluates, logging the error once per loop, item, and content hash. The first cycle after a restart or instance change fetches failing items again, so a fix applies without waiting for a change.
41
80
 
42
- Polling cursors live in the engine's `CursorStore`. When a loop starts watching a repository or its trigger expression changes, the orchestrator discards that repository's efficiency cursor and performs a cold poll, while the permanent Event ledger absorbs repeated state.
81
+ Cursors live in the engine's `CursorStore`. When a loop starts watching a repo or its trigger changes, the orchestrator drops that repo's cursor and polls cold; the event ledger absorbs repeats.
43
82
 
44
- PR-triggered Attempts fetch and check out the immutable `headRefOid` saved in their Event payload. A later push creates a new Event with its new head commit. Issue and schedule Attempts use the repository's default branch.
83
+ Before a queued event's run opens, the orchestrator re-checks its trigger against the item's GitHub state, with a five-second limit. A failed or timed-out check logs a warning and lets the event proceed; later polling can revoke the run. If GitHub no longer has the item, the engine revokes its queued events and marks it gone.
45
84
 
46
- `PollingTriggers.layer` requires `CursorStore`, `LoopControl`, `RunEngine`, `ConfigParser`, `ExpressionEngine`, `GitHubPolling`, the repository-label projection reader/writer, and `LabelReconciler`. `LabelReconciler.layer` in turn requires `GitHubLabelWriter`. The engine's local SQLite layer supplies the engine services:
85
+ PR-triggered attempts check out the immutable `headRefOid` saved in their event payload; a later push is a new event. Issue and schedule attempts use the default branch.
86
+
87
+ `PollingTriggers.layer` needs `CursorStore`, `LoopControl`, `RunEngine`, `ConfigParser`, `ExpressionEngine`, `GitHubPolling`, the item and repo-label projection readers and writers, and `LabelReconciler`, whose layer needs `GitHubLabelWriter`. The engine's local SQLite layer supplies the engine services:
47
88
 
48
89
  ```ts
49
90
  import { Layer } from "effect"
@@ -61,21 +102,44 @@ export const triggersLayer = PollingTriggers.layer.pipe(
61
102
  )
62
103
  ```
63
104
 
64
- Call `poll` when another scheduler owns timing. Use `run` for the built-in endless polling loop.
105
+ Call `poll` if another scheduler owns timing; `run` is the built-in endless loop.
65
106
 
66
107
  ## Schedule triggers
67
108
 
68
- `ScheduleTriggers.tick(instance)` evaluates the most recent due Tick for every `on: schedule` loop and fans it out to the repositories selected by that loop. Each selected repository receives one Event whose payload is `item.tick = { scheduledAt, scheduledAtLocal, cron, tz }`; `scheduledAt` is the UTC time the Tick was due, `scheduledAtLocal` the same instant in the loop's `tz:`, while the Event's `receivedAt` records when the orchestrator observed it.
109
+ `ScheduleTriggers.tick(instance)` evaluates the latest due tick of every `on: schedule` loop and fans it out to the loop's selected repos. Each repo gets one event with the payload `item.tick = { scheduledAt, scheduledAtLocal, cron, tz }`: `scheduledAt` is the UTC due time, `scheduledAtLocal` the same instant in the loop's `tz:`, and the event's `receivedAt` is when the orchestrator saw it.
110
+
111
+ The event key covers the loop, canonical repo, and scheduled time. An in-memory record of fired ticks makes repeated `tick` calls free; after downtime, a restart, or a reload, only the latest missed tick fires. A cron whose boundaries can't be computed at an instant (Effect's Cron can fail inside a DST spring-forward gap) fails only that schedule for that cycle, reported on the returned `ScheduleCycle`. The repo is the schedule stream's item, so a later tick follows the supersede rules of issue and PR state. Schedule runs use the same engine, approval gate, scheduler, executor, and agent layer; with no issue or PR to comment on, they block only in the UI.
112
+
113
+ `ScheduleTriggers.run(instances)` evaluates once at startup to catch up, then sleeps until the next tick, at most a minute, reading `instances.get` before each tick. A reload on `instances.changes` ends the sleep, so added or changed schedule loops are evaluated at once. Its layer needs `RunEngine`, `ConfigParser`, and `ExpressionEngine`.
69
114
 
70
- The Event key covers the loop, canonical repository, and scheduled time. An in-memory fired-tick memory skips already-submitted boundaries, so calling `tick` repeatedly costs nothing, and waking after downtime (or a restart, which clears that memory) emits only the latest missed Tick. A cron whose boundaries cannot be computed at an instant — effect's Cron can fail inside a DST spring-forward gap — fails only that schedule for that cycle and is reported on the returned `ScheduleCycle`. The repository is the schedule stream's Item, so a later Tick follows the same supersede rules as issue and pull-request state. Schedule Runs go through the unchanged `RunEngine`, approval gate, worker scheduler, executor, and agent layer. Because they have no GitHub issue or pull request to comment on, their blocking moments remain UI-only.
115
+ ## Instance reload
71
116
 
72
- `ScheduleTriggers.run(instance)` evaluates once at startup for catch-up, then sleeps until the earliest next Tick. Its layer requires `RunEngine`, `ConfigParser`, and `ExpressionEngine`.
117
+ `CurrentInstance` (`/current-instance`) is the running instance snapshot. `CurrentInstance.layer(directory, initial)` starts from the one `usine up` loaded, and needs `InstanceFolder` (`@usine/config/instance-folder`). Polling, schedules, queued-event re-checks, App project discovery, attempt settings, and the operator backend read `get` when they act.
118
+
119
+ `reload` loads the folder again with the same per-file quarantine and replaces the snapshot. It returns an `InstanceReload` that names the loops, agents, labels, repos, and workers added, removed, or changed, and the files left out. An invalid `usine.yaml` fails with `FactoryConfigError`, and a changed `webhooks` block with `RestartRequiredError`, whose `fields` and message name the change; both keep the running snapshot. `changes` streams the snapshot, then each one an accepted reload installs.
120
+
121
+ `registerWorkers(workers)` passes each later reload to the workers first. They refuse a `workers` change they can't apply (`RestartRequiredError`), preflight added workers, slots, and brains, refuse a failed preflight (`ReloadPreflightError`), and apply the new `workers` block after the snapshot is replaced. `stepExecutorLayer` registers itself; until then, as in setup mode, a reload refuses any `workers` change and preflights nothing. A run keeps the agent, task, and parent model it opened with.
73
122
 
74
123
  ## Worker scheduling
75
124
 
76
- `@usine/orchestrator/scheduler` exports the platform-free `stepExecutorLayer`. `stepExecutorLayer(config, brains)` accepts the roster’s `BrainSpec` values. At startup it verifies every configured worker against that roster, creates one capacity slot per `max_concurrent_attempts`, and assigns slots by Event receipt time. Runs stay pending while every slot is occupied. Each Attempt receives the configured `max_children` child-session budget. The layer rejects worker kinds that its supplied executor cannot run. Worker capability failures during preparation become non-retryable `AgentFailure` outcomes, like unavailable model credentials.
125
+ `stepExecutorLayer(config, brains, options)` (`/scheduler`, platform-free) takes the roster's `BrainSpec` values. At startup it checks every worker against the roster and the instance's agents, gives each `max_concurrent_attempts` slots, and assigns a free slot of the first worker in `usine.yaml` order to the run whose event arrived first. Runs stay pending while all slots are taken.
126
+
127
+ A reservation naming a worker, as a reattaching run's does, takes only that worker's slot, before unnamed ones; it waits while that worker is full, and later runs can take other workers. For an undeclared worker, it logs a warning and waits for a reload that declares it. `occupy` takes a slot at once for an attempt already running, as at engine startup, even with none free, logging a warning; the worker then takes no new attempt until enough end, and each extra slot retires with its attempt. An unknown worker gets no slot, with a warning.
128
+
129
+ The layer registers with `CurrentInstance`, so a reload changes workers without touching running attempts:
130
+
131
+ - A worker with the same name, kind, and settings and a higher `max_concurrent_attempts` is preflighted with the new count and every rostered brain, and its new slots take waiting runs at once. With a lower count, it takes new attempts only while it runs fewer than the new count, and each extra slot retires with its attempt.
132
+ - A new worker of the executor's kind is preflighted with every brain and takes attempts once it passes. With `options.workers`, as the SSH composition sets, it joins the executor before preflight and leaves if the reload is refused.
133
+ - A removed worker takes no new attempt and stays in `StepExecutor.workers` until its last attempt ends; only then can its leftovers be discarded. Until then, its `UnconfiguredWorkerNeed` counts its running attempts as `running`, and `Operator.discardLeftovers` fails with `LeftoversDiscardRejectedError`. With `options.workers`, it then leaves the executor too.
134
+ - A changed kind or other setting, a renamed worker, or a worker the executor can't run fails the reload with `RestartRequiredError` naming the field, such as `workers.build-1.destination`.
135
+
136
+ Other workers are preflighted with the brains the reload adds; any failed preflight refuses the reload. Resizes, additions, removals, and retired slots are logged with counts. Attempts read `max_children`, the runaway ceilings, and the parent effort from `CurrentInstance` at start, so reloads apply to the next attempt. The layer rejects worker kinds its executor can't run. Capability failures during preparation become non-retryable `AgentFailure` outcomes, like missing model credentials.
137
+
138
+ The layer also provides `WorkerStatus` (`/worker-status`): each worker's slots and how many are taken, and the last preflight of each worker's own prerequisites and of each harness, with time and result. It records the startup preflight and every reload's, refused ones included. The local, SSH, and exe.dev compositions pass it on; `WorkerStatus.idle` stands in where no worker runs attempts.
139
+
140
+ ## Compositions
77
141
 
78
- `@usine/orchestrator/local` composes the scheduler with the Node local executor and the engine's local SQLite adapter. It requires a caller-owned `CredentialProvider`; use `gitHubCredentialLayer` with `@usine/github`, or supply another provider that mints one repository credential per Attempt.
142
+ `/local` composes the scheduler with the Node local executor and the engine's local SQLite adapter. It needs a caller-owned `CredentialProvider`: `gitHubCredentialLayer` with `@usine/github`, or another provider that mints one repo credential per attempt.
79
143
 
80
144
  ```ts
81
145
  import { layer as localLayer } from "@usine/orchestrator/local"
@@ -85,11 +149,54 @@ const EngineLive = localLayer(instanceDirectory, config, brains, {
85
149
  })
86
150
  ```
87
151
 
88
- Provide `CredentialProvider` to `EngineLive`, then request `RunEngine` from the resulting Effect layer. The local composition caches repositories under `<instance>/.usine/workers/<worker>/repos` and removes Attempt worktrees after completion or cancellation.
152
+ Provide `CredentialProvider` and `CurrentInstance` to `EngineLive`, then request `RunEngine`. Repos are cached under `<instance>/.usine/workers/<worker>/repos`; worktrees are removed after completion or cancel.
153
+
154
+ All three compositions run each attempt's Pi parent in the orchestrator as a durable task (`@usine/durable`). `layer` needs the `AttemptRuntime` and the `ClaudeLoginLock` shared by its Claude children and the worker's preflight, and builds the runtime before the engine, so tasks are resumed before run recovery and a restart reattaches live attempts. Stored v3–v6 transcripts stay readable.
155
+
156
+ `runtimeLayer(instanceDirectory)` (`/runtime`) composes the whole application, adding GitHub polling, schedule triggers, CEL parsing, `Orchestrator`, and `Operator`. It uses the SSH composition if every worker is `ssh`, otherwise the local one, which refuses other kinds at startup. One `ClaudeLoginLock` serves both the attempt runtime and the worker composition. Preflight requirements come from the instance's agents at build time. It needs `GitHubCredentials` and `CurrentInstance`, which the host loads. `operatorObservationLayer(instanceDirectory)` is the read-only Node composition for setup and maintenance: it provides `OperatorObservation` and `OperatorConfiguration` from durable runs and loop state, with triggers idle and no worker running attempts.
157
+
158
+ `/ssh` composes the scheduler with the SSH executor, and also needs a `CredentialProvider` for the token each attempt writes on its machine or attaches to its gateway. `sshWorkersOf(config)` returns the declared SSH workers; `sshWorkerAddressesOf(config)` turns them into executor addresses, reading each gateway's admin token from its `admin_token_env` variable, and fails with a `gateway` capability error naming an unset one. Admin APIs are called with `fetch` through the executor's forward to each gateway's `destination`.
159
+
160
+ `sshStepExecutorLayer(config, brains)` lets a reload resize, add, and remove SSH workers through `SshWorkers`. An added worker joins the executor before preflight, and its gateway's token is read then; an unset variable refuses the reload with `ReloadPreflightError` naming it, since a running process doesn't see variables set after it started. A removed worker leaves the executor once its last attempt ends, and its gateway's forward closes if no other worker uses it.
161
+
162
+ ```ts
163
+ import { layer as sshLayer, sshWorkerAddressesOf } from "@usine/orchestrator/ssh"
164
+ import { Effect, Layer } from "effect"
165
+
166
+ const EngineLive = Layer.unwrap(
167
+ Effect.map(sshWorkerAddressesOf(config), (workers) =>
168
+ sshLayer(instanceDirectory, config, brains, { workers }),
169
+ ),
170
+ )
171
+ ```
172
+
173
+ The live SSH test runs one attempt on a real machine, with a scripted parent using the machine's gh and git with the minted token, and preflights Claude Code and Codex there, printing `ready` or why a login is missing. It runs only if `USINE_SSH_INTEGRATION_DESTINATION` is set, and needs an instance directory whose App installation covers the repo:
89
174
 
90
- For the whole local application, `runtimeLayer(instanceDirectory, instance, options)` additionally composes GitHub polling, schedule triggers, CEL parsing, `Orchestrator`, and `Operator`. It derives worker preflight requirements from `instance.agents` and requires only `GitHubCredentials`; the server or another host owns how those credentials are loaded. `operatorObservationLayer(instanceDirectory)` is the read-only Node composition for setup and maintenance surfaces: it reads durable Runs and Loop runtime state while trigger execution remains idle.
175
+ ```sh
176
+ export USINE_SSH_INTEGRATION_DESTINATION=usine@203.0.113.7
177
+ export USINE_SSH_INTEGRATION_IDENTITY_FILE=~/.ssh/usine_worker # optional
178
+ export USINE_SSH_INTEGRATION_KNOWN_HOSTS=/path/to/known_hosts # optional
179
+ export USINE_SSH_INTEGRATION_ROOT=/home/usine/.usine # the default
180
+ export USINE_SSH_INTEGRATION_REPO=owner/repo
181
+ export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
182
+ pnpm --filter @usine/orchestrator test:ssh
183
+ ```
91
184
 
92
- For an exe.dev Worker, use the `@usine/orchestrator/exe-dev` composition. It keeps the engine and SQLite runtime local while the unchanged Worker contract runs on the named VM:
185
+ The same file tests a worker behind a gateway (`infra/hetzner/gateway`). The scripted child fetches through the gateway, calls `gh api`, pushes and deletes a `usine-gateway-test/<uuid>` branch through a `https://github.com/` URL, reaches the npm registry through the forward proxy, and looks for GitHub tokens on the machine; the test then checks that the gateway has no attachment for the worker. The repo must accept the App's pushes. It runs only if `USINE_GATEWAY_INTEGRATION_DESTINATION` is set. Besides the executor's own forward, the post-attempt check opens one on local port 17070:
186
+
187
+ ```sh
188
+ export USINE_GATEWAY_INTEGRATION_DESTINATION=worker-one # a Host with ProxyJump through the gateway
189
+ export USINE_GATEWAY_INTEGRATION_SSH_CONFIG=/path/to/ssh_config # optional
190
+ export USINE_GATEWAY_INTEGRATION_ROOT=/home/usine/.usine # the default
191
+ export USINE_GATEWAY_INTEGRATION_DOMAIN=gw.internal # the default
192
+ export USINE_GATEWAY_INTEGRATION_GATEWAY=usine-gateway # the default: the Gateway's SSH destination
193
+ export USINE_GATEWAY_INTEGRATION_ADMIN_TOKEN=... # from /etc/usine-gateway/secrets.env
194
+ export USINE_GATEWAY_INTEGRATION_REPO=owner/repo
195
+ export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
196
+ pnpm --filter @usine/orchestrator test:ssh
197
+ ```
198
+
199
+ For exe.dev, `/exe-dev` keeps the engine and SQLite runtime local and runs the worker contract on the named VM:
93
200
 
94
201
  ```ts
95
202
  import { RepositoryName } from "@usine/core/repository"
@@ -107,9 +214,9 @@ const EngineLive = exeDevLayer(instanceDirectory, config, brains, {
107
214
  })
108
215
  ```
109
216
 
110
- The composition sets the GitHub integration grant to five minutes beyond `max_attempt_minutes`. Pass `credentialGrantMinutes` to use another bounded window. The scheduler also bounds every launch at `max_attempt_minutes`: a hung run host or dead link is interrupted through the normal cancel handshake and reported as a typed infrastructure failure before its credential grant lapses.
217
+ The integration grant is a renewal window, not an attempt bound: it lasts `credentialGrantMinutes` (65 by default), renews at half that while the attempt runs, and retries a failed renewal every minute, whatever `max_attempt_minutes` is. If `max_attempt_minutes` is set, the scheduler bounds every launch at it: a hung parent or dead link is interrupted through the normal cancel and reported as a typed infrastructure failure.
111
218
 
112
- The live exe.dev check requires an existing baked VM and its GitHub integration:
219
+ The live exe.dev test needs an existing baked VM and its GitHub integration:
113
220
 
114
221
  ```sh
115
222
  export USINE_EXE_DEV_INTEGRATION_WORKER=worker-name
@@ -119,9 +226,9 @@ export USINE_EXE_DEV_INTEGRATION_GITHUB_INTEGRATION=integration-name
119
226
  pnpm --filter @usine/orchestrator test:exe-dev
120
227
  ```
121
228
 
122
- ## Live GitHub check
229
+ ## Live GitHub tests
123
230
 
124
- The opt-in integration test needs a disposable issue in a repository covered by the configured GitHub App. It creates a uniquely named repository label, applies it to the issue, checks the resulting Event and restart deduplication, then deletes the label.
231
+ The opt-in integration test needs a disposable issue in a repo that the configured App covers. It creates a uniquely named label, applies it to the issue, checks the resulting event and restart deduplication, then deletes the label.
125
232
 
126
233
  ```sh
127
234
  export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
@@ -130,7 +237,7 @@ export USINE_GITHUB_INTEGRATION_ISSUE_NUMBER=123
130
237
  pnpm --filter @usine/orchestrator test:integration
131
238
  ```
132
239
 
133
- The agent contract test also starts the local worker and production run host. The Pi parent delegates to Claude Code, which reads the issue with `gh` and posts one signed comment as the App bot. Build the workspace first, provide working Pi and Claude credentials in the worker user's home directory, then run:
240
+ The agent contract test starts the local worker with the production durable parent runtime. The Pi parent delegates to Claude Code, which reads the issue with `gh` and posts one signed comment as the App bot. Build the workspace, provide working Pi and Claude credentials in the worker user's home directory, then run:
134
241
 
135
242
  ```sh
136
243
  pnpm build
@@ -140,12 +247,4 @@ export USINE_AGENT_INTEGRATION_CHILD_MODEL=claude-model-id
140
247
  pnpm --filter @usine/orchestrator test:integration:agent
141
248
  ```
142
249
 
143
- The test removes its kick and result comments during cleanup.
144
-
145
- Everything here is 0.x, so APIs can change between minor releases. Supply `effect` at the version pinned by the usine workspace.
146
-
147
- Item cycle exhaustion appears as a `CycleBudgetNeed` in the overview and marks the Item’s Board card as needing attention. `Operator.resetCycleBudget` records an answer to the exact Ask; polling restarts the repository cursor, fetches current Items, and evaluates active Loops before opening further Runs. A failed refresh is retried. Each Item fetch captures its budget generation; resets supersede in-flight observations and fresh Events use the current Loop configuration. GitHub delivery of these Asks is tracked separately in [#36](https://github.com/basaltbytes/usine/issues/36).
148
-
149
- The operator HTTP API exposes `POST /api/cycle-budgets/reset` with `{ "askId": "…" }`. A stale or differently answered Ask returns HTTP 409.
150
-
151
- The orchestrator requires run-host Attempt channel protocol v6 for saved harness session metadata. Upgrade both together; worker preflight rejects a version mismatch. Stored v3–v5 transcripts remain readable.
250
+ Cleanup removes its kick and result comments.
@@ -2,7 +2,7 @@ import { GitHubLabelName } from "@usine/config/label";
2
2
  import type { LaneItemState } from "@usine/config/board";
3
3
  import { ItemKey, ItemNumber } from "@usine/core/item";
4
4
  import type { ItemProjection } from "@usine/engine/item-projection";
5
- import { Effect } from "effect";
5
+ import { Effect, Schema } from "effect";
6
6
  import { BoardProjectionError } from "../operator/board.ts";
7
7
  export interface ProjectedItem {
8
8
  readonly itemKey: ItemKey;
@@ -13,6 +13,14 @@ export interface ProjectedItem {
13
13
  readonly state: LaneItemState;
14
14
  readonly linkedPullRequests: ReadonlyArray<string>;
15
15
  readonly labels: ReadonlyArray<GitHubLabelName>;
16
+ /** Whether GitHub reports the pull request branch as conflicting with its base. */
17
+ readonly conflicting: boolean;
16
18
  }
19
+ /**
20
+ * Returns the labels in an Item `payload` of `kind`, such as the payload of a Run's Event. Fails if
21
+ * the payload has no labels of that shape.
22
+ */
23
+ export declare const payloadLabels: (kind: ProjectedItem["kind"], payload: Schema.Json) => Effect.Effect<readonly (string & import("effect/Brand").Brand<"GitHubLabelName">)[], Schema.SchemaError, never>;
24
+ /** Returns the Board's view of one stored Item. An Item gone from GitHub counts as closed. */
17
25
  export declare const projectedItem: (item: ItemProjection) => Effect.Effect<ProjectedItem, BoardProjectionError>;
18
26
  //# sourceMappingURL=items.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEpE,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACjD;AAWD,eAAO,MAAM,aAAa,SAClB,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,oBAAoB,CA0ChD,CAAC"}
1
+ {"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAEpE,OAAO,EAAE,MAAM,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAChD,mFAAmF;IACnF,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAQD;;;GAGG;AACH,eAAO,MAAM,aAAa,SAAU,aAAa,CAAC,MAAM,CAAC,WAAW,MAAM,CAAC,IAAI,oHAG5E,CAAC;AASJ,8FAA8F;AAC9F,eAAO,MAAM,aAAa,SAClB,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,oBAAoB,CA6ChD,CAAC"}
@@ -1,15 +1,25 @@
1
1
  import { GitHubLabelName } from "@usine/config/label";
2
2
  import { ItemKey, ItemNumber } from "@usine/core/item";
3
3
  import { IssuePayload, PullRequestPayload } from "@usine/github/polling";
4
- import { Effect, Schema } from "effect";
4
+ import { Effect, Option, Schema } from "effect";
5
5
  import { BoardProjectionError } from "../operator/board.js";
6
6
  const labelsFrom = Schema.decodeUnknownEffect(Schema.Array(GitHubLabelName));
7
+ // Only the labels are read, so a payload stored before later fields were added still decodes.
8
+ const IssueLabels = Schema.Struct({ labels: IssuePayload.fields.labels });
9
+ const PullRequestLabels = Schema.Struct({ labels: PullRequestPayload.fields.labels });
10
+ /**
11
+ * Returns the labels in an Item `payload` of `kind`, such as the payload of a Run's Event. Fails if
12
+ * the payload has no labels of that shape.
13
+ */
14
+ export const payloadLabels = (kind, payload) => Schema.decodeUnknownEffect(kind === "issue" ? IssueLabels : PullRequestLabels)(payload).pipe(Effect.flatMap(({ labels }) => labelsFrom(labels.nodes.map(({ name }) => name))));
7
15
  const projectionError = (item, message) => new BoardProjectionError({
8
16
  repository: item.repository,
9
17
  itemKey: item.itemKey,
10
18
  message,
11
19
  });
20
+ /** Returns the Board's view of one stored Item. An Item gone from GitHub counts as closed. */
12
21
  export const projectedItem = (item) => Effect.gen(function* () {
22
+ const gone = Option.isSome(item.goneAt);
13
23
  if (item.source === "issue") {
14
24
  const payload = yield* Schema.decodeUnknownEffect(IssuePayload)(item.payload).pipe(Effect.mapError((error) => projectionError(item, error.message)));
15
25
  return {
@@ -18,16 +28,17 @@ export const projectedItem = (item) => Effect.gen(function* () {
18
28
  number: ItemNumber.make(item.number),
19
29
  title: payload.title,
20
30
  url: payload.url,
21
- state: payload.state === "OPEN" ? "issue-open" : "issue-closed",
31
+ state: payload.state === "OPEN" && !gone ? "issue-open" : "issue-closed",
22
32
  linkedPullRequests: payload.closedByPullRequestsReferences.nodes.map(({ url }) => url),
23
33
  labels: yield* labelsFrom(payload.labels.nodes.map(({ name }) => name)).pipe(Effect.mapError((error) => projectionError(item, error.message))),
34
+ conflicting: false,
24
35
  };
25
36
  }
26
37
  if (item.source === "pr") {
27
38
  const payload = yield* Schema.decodeUnknownEffect(PullRequestPayload)(item.payload).pipe(Effect.mapError((error) => projectionError(item, error.message)));
28
39
  const state = payload.merged
29
40
  ? "pr-merged"
30
- : payload.state === "OPEN"
41
+ : payload.state === "OPEN" && !gone
31
42
  ? "pr-open"
32
43
  : "pr-closed";
33
44
  return {
@@ -39,8 +50,9 @@ export const projectedItem = (item) => Effect.gen(function* () {
39
50
  state,
40
51
  linkedPullRequests: [],
41
52
  labels: yield* labelsFrom(payload.labels.nodes.map(({ name }) => name)).pipe(Effect.mapError((error) => projectionError(item, error.message))),
53
+ conflicting: state === "pr-open" && payload.mergeable === "CONFLICTING",
42
54
  };
43
55
  }
44
- return yield* projectionError(item, `Unsupported Item projection source: ${item.source}`);
56
+ return yield* projectionError(item, `Unsupported item projection source: ${item.source}`);
45
57
  });
46
58
  //# sourceMappingURL=items.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"items.js","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"items.js","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,15 @@
1
1
  import type { ManagedRepo } from "@usine/config/factory";
2
+ import type { Event } from "@usine/core/event";
2
3
  import type { ItemProjection } from "@usine/engine/item-projection";
3
4
  import { ItemLabelMutation } from "@usine/github/item-writer";
4
5
  import { Effect } from "effect";
5
6
  import { BoardView, WritableBoardLaneView } from "../operator/board.ts";
6
7
  import { type RunView } from "../operator/run.ts";
7
- export declare const projectBoard: (repository: ManagedRepo, repositoryLabels: readonly string[], storedItems: readonly ItemProjection[], runs: readonly RunView[], cycleBlockedItems?: readonly (string & import("effect/Brand").Brand<"ItemKey">)[] | undefined) => Effect.Effect<BoardView, never, never>;
8
+ import type { RunUsageView } from "../operator/usage.ts";
9
+ /**
10
+ * Groups the repository's stored Items into Task cards and the cards into lanes. A card with a
11
+ * running Run stays in the lane its trigger matched, read from `runEvents`.
12
+ */
13
+ export declare const projectBoard: (repository: ManagedRepo, repositoryLabels: readonly string[], storedItems: readonly ItemProjection[], runs: readonly RunView[], cycleBlockedItems?: readonly (string & import("effect/Brand").Brand<"ItemKey">)[] | undefined, itemLoops?: ReadonlyMap<string & import("effect/Brand").Brand<"ItemKey">, readonly (string & import("effect/Brand").Brand<"LoopName">)[]> | undefined, runUsage?: ReadonlyMap<string & import("effect/Brand").Brand<"RunId">, RunUsageView> | undefined, runEvents?: ReadonlyMap<string & import("effect/Brand").Brand<"RunId">, Event> | undefined) => Effect.Effect<BoardView, never, never>;
8
14
  export declare const labelMutationForBoardMove: (board: BoardView, target: WritableBoardLaneView) => ItemLabelMutation;
9
15
  //# sourceMappingURL=projection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/board/projection.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAIL,SAAS,EAET,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAgElD,eAAO,MAAM,YAAY,2QAwExB,CAAC;AAEF,eAAO,MAAM,yBAAyB,UAC7B,SAAS,UACR,qBAAqB,KAC5B,iBAaF,CAAC"}
1
+ {"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/board/projection.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAI/C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAkB,MAAM,QAAQ,CAAC;AAChD,OAAO,EAIL,SAAS,EAET,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AA+GzD;;;GAGG;AACH,eAAO,MAAM,YAAY,gmBAiFxB,CAAC;AAEF,eAAO,MAAM,yBAAyB,UAC7B,SAAS,UACR,qBAAqB,KAC5B,iBAaF,CAAC"}
@@ -1,11 +1,11 @@
1
- import { GitHubLabelName } from "@usine/config/label";
2
- import { LabelLaneSource, Lane, LaneName, } from "@usine/config/board";
1
+ import { canonicalLabelName, GitHubLabelName } from "@usine/config/label";
2
+ import { isSettledItemState, LabelLaneSource, Lane, LaneName, } from "@usine/config/board";
3
3
  import { ItemKey } from "@usine/core/item";
4
4
  import { ItemLabelMutation } from "@usine/github/item-writer";
5
- import { Effect, Result } from "effect";
5
+ import { Effect, Option, Result } from "effect";
6
6
  import { BoardTaskView, BoardLaneId, BoardProjectionError, BoardView, ReadOnlyBoardLaneView, WritableBoardLaneView, } from "../operator/board.js";
7
7
  import {} from "../operator/run.js";
8
- import { projectedItem } from "./items.js";
8
+ import { payloadLabels, projectedItem } from "./items.js";
9
9
  import { taskCards } from "./tasks.js";
10
10
  const labelSources = (sources) => sources.flatMap((source) => ("label" in source ? [source.label] : []));
11
11
  const stateSources = (sources) => sources.flatMap((source) => ("state" in source ? [source.state] : []));
@@ -25,14 +25,48 @@ const effectiveLanes = (repository, repositoryLabels) => repository.lanes.length
25
25
  ? repository.lanes.map((lane) => ({ lane, origin: "authored" }))
26
26
  : proposedLanes(repositoryLabels);
27
27
  const laneId = (definition) => BoardLaneId.make(`${definition.origin}:${definition.lane.name}`);
28
- const SETTLED_STATES = ["pr-merged", "pr-closed", "issue-closed"];
29
- const isSettled = (item) => SETTLED_STATES.includes(item.state);
30
- const labelLaneFor = (item, lanes) => lanes.find(({ lane }) => labelSources(lane.sources).some((label) => item.labels.includes(label)));
28
+ // GitHub matches label names ignoring case, as the label declarations do.
29
+ const labelLaneFor = (item, lanes) => {
30
+ const itemLabels = new Set(item.labels.map(canonicalLabelName));
31
+ return lanes.find(({ lane }) => labelSources(lane.sources).some((label) => itemLabels.has(canonicalLabelName(label))));
32
+ };
31
33
  const stateLaneFor = (item, lanes) => lanes.find(({ lane }) => stateSources(lane.sources).includes(item.state));
32
- const laneFor = (item, lanes) => isSettled(item)
34
+ const laneFor = (item, lanes) => isSettledItemState(item.state)
33
35
  ? stateLaneFor(item, lanes)
34
36
  : (labelLaneFor(item, lanes) ?? stateLaneFor(item, lanes));
35
- export const projectBoard = Effect.fn("Board.project")((repository, repositoryLabels, storedItems, runs, cycleBlockedItems = []) => Effect.gen(function* () {
37
+ /**
38
+ * Returns the label lane that matched when `running` was triggered, from its Event's labels, or
39
+ * `undefined` if the current Item is closed or merged or no lane can be read from the Event.
40
+ */
41
+ const triggerLaneFor = Effect.fn("Board.triggerLane")(function* (repository, view, running, lanes, runEvents) {
42
+ const fields = {
43
+ repository: repository.name,
44
+ itemKey: view.itemKey,
45
+ runId: running.id,
46
+ loop: running.loop,
47
+ };
48
+ yield* Effect.annotateCurrentSpan(fields);
49
+ if (isSettledItemState(view.state))
50
+ return undefined;
51
+ const event = runEvents.get(running.id);
52
+ if (event === undefined) {
53
+ yield* Effect.logDebug("No Event read for the card's running Run; placing it by its Item's labels", fields);
54
+ return undefined;
55
+ }
56
+ const labels = yield* payloadLabels(view.kind, event.payload).pipe(Effect.map(Option.some), Effect.catch((error) => Effect.logWarning("The running Run's Event has no labels to read; placing its card by its Item's labels", { ...fields, eventId: event.id, errorTag: error._tag, message: error.message }).pipe(Effect.as(Option.none()))));
57
+ if (Option.isNone(labels))
58
+ return undefined;
59
+ const lane = labelLaneFor({ labels: labels.value }, lanes);
60
+ yield* Effect.logDebug(lane === undefined
61
+ ? "No lane label when the card's running Run was triggered; placing it by its Item's labels"
62
+ : "Keeping the card in the lane its running Run was triggered from", { ...fields, lane: lane?.lane.name ?? "none" });
63
+ return lane;
64
+ });
65
+ /**
66
+ * Groups the repository's stored Items into Task cards and the cards into lanes. A card with a
67
+ * running Run stays in the lane its trigger matched, read from `runEvents`.
68
+ */
69
+ export const projectBoard = Effect.fn("Board.project")((repository, repositoryLabels, storedItems, runs, cycleBlockedItems = [], itemLoops = new Map(), runUsage = new Map(), runEvents = new Map()) => Effect.gen(function* () {
36
70
  yield* Effect.annotateCurrentSpan({ repository: repository.name });
37
71
  const results = yield* Effect.forEach(storedItems, (item) => projectedItem(item).pipe(Effect.result));
38
72
  const items = [];
@@ -51,23 +85,28 @@ export const projectBoard = Effect.fn("Board.project")((repository, repositoryLa
51
85
  });
52
86
  }
53
87
  const lanes = effectiveLanes(repository, repositoryLabels);
54
- const cards = taskCards(repository.name, items, runs, cycleBlockedItems);
88
+ const cards = taskCards(repository.name, items, runs, cycleBlockedItems, itemLoops, runUsage);
55
89
  yield* Effect.logDebug("Projected Task cards from linked Items", {
56
90
  repository: repository.name,
57
91
  items: items.length,
58
92
  tasks: cards.length,
59
93
  runs: runs.length,
94
+ watchedTasks: cards.filter(({ view }) => view.loops.length > 0).length,
95
+ runningTasks: cards.filter(({ running }) => running !== undefined).length,
60
96
  });
61
97
  const grouped = new Map();
62
98
  for (const definition of lanes)
63
99
  grouped.set(laneId(definition), []);
64
100
  const unmatched = [];
65
- for (const card of cards) {
66
- const definition = laneFor(card, lanes);
101
+ for (const { view, running } of cards) {
102
+ const triggerLane = running === undefined
103
+ ? undefined
104
+ : yield* triggerLaneFor(repository, view, running, lanes, runEvents);
105
+ const definition = triggerLane ?? laneFor(view, lanes);
67
106
  if (definition === undefined)
68
- unmatched.push(card);
107
+ unmatched.push(view);
69
108
  else
70
- grouped.get(laneId(definition))?.push(card);
109
+ grouped.get(laneId(definition))?.push(view);
71
110
  }
72
111
  const sortItems = (values) => values.sort((left, right) => right.number - left.number);
73
112
  return new BoardView({