@usine/orchestrator 0.2.1 → 0.4.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.
- package/LICENSE +661 -21
- package/README.md +174 -52
- package/dist/board/items.d.ts +22 -3
- package/dist/board/items.d.ts.map +1 -1
- package/dist/board/items.js +44 -4
- package/dist/board/items.js.map +1 -1
- package/dist/board/projection.d.ts +8 -1
- package/dist/board/projection.d.ts.map +1 -1
- package/dist/board/projection.js +63 -14
- package/dist/board/projection.js.map +1 -1
- package/dist/board/tasks.d.ts +15 -3
- package/dist/board/tasks.d.ts.map +1 -1
- package/dist/board/tasks.js +28 -9
- package/dist/board/tasks.js.map +1 -1
- package/dist/cloudflare.d.ts +32 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +68 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/credentials.d.ts +5 -6
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +22 -15
- package/dist/credentials.js.map +1 -1
- package/dist/current-instance/changes.d.ts +87 -0
- package/dist/current-instance/changes.d.ts.map +1 -0
- package/dist/current-instance/changes.js +141 -0
- package/dist/current-instance/changes.js.map +1 -0
- package/dist/current-instance/current-instance.d.ts +89 -0
- package/dist/current-instance/current-instance.d.ts.map +1 -0
- package/dist/current-instance/current-instance.js +163 -0
- package/dist/current-instance/current-instance.js.map +1 -0
- package/dist/exe-dev.d.ts +6 -2
- package/dist/exe-dev.d.ts.map +1 -1
- package/dist/exe-dev.js +10 -9
- package/dist/exe-dev.js.map +1 -1
- package/dist/http-client.d.ts +8 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +8 -0
- package/dist/http-client.js.map +1 -0
- package/dist/idle-triggers.d.ts +1 -1
- package/dist/idle-triggers.d.ts.map +1 -1
- package/dist/idle-triggers.js +1 -1
- package/dist/idle-triggers.js.map +1 -1
- package/dist/label-reconciler.d.ts +5 -2
- package/dist/label-reconciler.d.ts.map +1 -1
- package/dist/label-reconciler.js.map +1 -1
- package/dist/lease-order.d.ts +60 -0
- package/dist/lease-order.d.ts.map +1 -0
- package/dist/lease-order.js +76 -0
- package/dist/lease-order.js.map +1 -0
- package/dist/local.d.ts +4 -18
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +9 -45
- package/dist/local.js.map +1 -1
- package/dist/loop-matches.d.ts +28 -0
- package/dist/loop-matches.d.ts.map +1 -0
- package/dist/loop-matches.js +74 -0
- package/dist/loop-matches.js.map +1 -0
- package/dist/operator/attempt.d.ts +76 -10
- package/dist/operator/attempt.d.ts.map +1 -1
- package/dist/operator/attempt.js +69 -11
- package/dist/operator/attempt.js.map +1 -1
- package/dist/operator/board.d.ts +48 -16
- package/dist/operator/board.d.ts.map +1 -1
- package/dist/operator/board.js +48 -17
- package/dist/operator/board.js.map +1 -1
- package/dist/operator/configuration.d.ts +314 -0
- package/dist/operator/configuration.d.ts.map +1 -0
- package/dist/operator/configuration.js +490 -0
- package/dist/operator/configuration.js.map +1 -0
- package/dist/operator/desk.d.ts +206 -0
- package/dist/operator/desk.d.ts.map +1 -0
- package/dist/operator/desk.js +525 -0
- package/dist/operator/desk.js.map +1 -0
- package/dist/operator/github-changes.d.ts +88 -0
- package/dist/operator/github-changes.d.ts.map +1 -0
- package/dist/operator/github-changes.js +266 -0
- package/dist/operator/github-changes.js.map +1 -0
- package/dist/operator/http.d.ts +135 -105
- package/dist/operator/http.d.ts.map +1 -1
- package/dist/operator/http.js +155 -30
- package/dist/operator/http.js.map +1 -1
- package/dist/operator/machines.d.ts +71 -0
- package/dist/operator/machines.d.ts.map +1 -0
- package/dist/operator/machines.js +103 -0
- package/dist/operator/machines.js.map +1 -0
- package/dist/operator/need.d.ts +58 -11
- package/dist/operator/need.d.ts.map +1 -1
- package/dist/operator/need.js +61 -9
- package/dist/operator/need.js.map +1 -1
- package/dist/operator/operator.d.ts +135 -29
- package/dist/operator/operator.d.ts.map +1 -1
- package/dist/operator/operator.js +370 -63
- package/dist/operator/operator.js.map +1 -1
- package/dist/operator/overview.d.ts +7 -7
- package/dist/operator/overview.d.ts.map +1 -1
- package/dist/operator/overview.js +5 -5
- package/dist/operator/overview.js.map +1 -1
- package/dist/operator/projection.d.ts +56 -5
- package/dist/operator/projection.d.ts.map +1 -1
- package/dist/operator/projection.js +108 -9
- package/dist/operator/projection.js.map +1 -1
- package/dist/operator/run.d.ts +101 -6
- package/dist/operator/run.d.ts.map +1 -1
- package/dist/operator/run.js +100 -7
- package/dist/operator/run.js.map +1 -1
- package/dist/operator/statistics/cache.d.ts +7 -0
- package/dist/operator/statistics/cache.d.ts.map +1 -0
- package/dist/operator/statistics/cache.js +40 -0
- package/dist/operator/statistics/cache.js.map +1 -0
- package/dist/operator/statistics/runs.d.ts +36 -0
- package/dist/operator/statistics/runs.d.ts.map +1 -0
- package/dist/operator/statistics/runs.js +284 -0
- package/dist/operator/statistics/runs.js.map +1 -0
- package/dist/operator/statistics/statistics.d.ts +20 -0
- package/dist/operator/statistics/statistics.d.ts.map +1 -0
- package/dist/operator/statistics/statistics.js +138 -0
- package/dist/operator/statistics/statistics.js.map +1 -0
- package/dist/operator/statistics/tasks.d.ts +30 -0
- package/dist/operator/statistics/tasks.d.ts.map +1 -0
- package/dist/operator/statistics/tasks.js +244 -0
- package/dist/operator/statistics/tasks.js.map +1 -0
- package/dist/operator/statistics/view.d.ts +302 -0
- package/dist/operator/statistics/view.d.ts.map +1 -0
- package/dist/operator/statistics/view.js +243 -0
- package/dist/operator/statistics/view.js.map +1 -0
- package/dist/operator/statistics/window.d.ts +24 -0
- package/dist/operator/statistics/window.d.ts.map +1 -0
- package/dist/operator/statistics/window.js +47 -0
- package/dist/operator/statistics/window.js.map +1 -0
- package/dist/operator/task.d.ts +94 -0
- package/dist/operator/task.d.ts.map +1 -0
- package/dist/operator/task.js +204 -0
- package/dist/operator/task.js.map +1 -0
- package/dist/operator/transcript.d.ts +12 -12
- package/dist/operator/transcript.d.ts.map +1 -1
- package/dist/operator/transcript.js +11 -9
- package/dist/operator/transcript.js.map +1 -1
- package/dist/operator/usage.d.ts +103 -0
- package/dist/operator/usage.d.ts.map +1 -0
- package/dist/operator/usage.js +234 -0
- package/dist/operator/usage.js.map +1 -0
- package/dist/orchestrator.d.ts +8 -5
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +4 -2
- package/dist/orchestrator.js.map +1 -1
- package/dist/polling/closed-item-asks.d.ts +8 -0
- package/dist/polling/closed-item-asks.d.ts.map +1 -0
- package/dist/polling/closed-item-asks.js +28 -0
- package/dist/polling/closed-item-asks.js.map +1 -0
- package/dist/polling/cycle.d.ts +28 -10
- package/dist/polling/cycle.d.ts.map +1 -1
- package/dist/polling/cycle.js +20 -6
- package/dist/polling/cycle.js.map +1 -1
- package/dist/polling/evaluation-failures.d.ts +42 -0
- package/dist/polling/evaluation-failures.d.ts.map +1 -0
- package/dist/polling/evaluation-failures.js +103 -0
- package/dist/polling/evaluation-failures.js.map +1 -0
- package/dist/polling/hints.d.ts +2 -2
- package/dist/polling/hints.d.ts.map +1 -1
- package/dist/polling/hints.js +2 -2
- package/dist/polling/hints.js.map +1 -1
- package/dist/polling/loop-evaluation.d.ts +21 -11
- package/dist/polling/loop-evaluation.d.ts.map +1 -1
- package/dist/polling/loop-evaluation.js +70 -27
- package/dist/polling/loop-evaluation.js.map +1 -1
- package/dist/polling/open-issues.d.ts +23 -0
- package/dist/polling/open-issues.d.ts.map +1 -0
- package/dist/polling/open-issues.js +77 -0
- package/dist/polling/open-issues.js.map +1 -0
- package/dist/polling/open-pull-requests.d.ts +28 -0
- package/dist/polling/open-pull-requests.d.ts.map +1 -0
- package/dist/polling/open-pull-requests.js +131 -0
- package/dist/polling/open-pull-requests.js.map +1 -0
- package/dist/polling/polling.d.ts +20 -11
- package/dist/polling/polling.d.ts.map +1 -1
- package/dist/polling/polling.js +215 -52
- package/dist/polling/polling.js.map +1 -1
- package/dist/polling/repository-sync.d.ts +21 -14
- package/dist/polling/repository-sync.d.ts.map +1 -1
- package/dist/polling/repository-sync.js +35 -28
- package/dist/polling/repository-sync.js.map +1 -1
- package/dist/polling-triggers.d.ts +1 -1
- package/dist/polling-triggers.d.ts.map +1 -1
- package/dist/polling-triggers.js +1 -1
- package/dist/polling-triggers.js.map +1 -1
- package/dist/reloads.d.ts +14 -0
- package/dist/reloads.d.ts.map +1 -0
- package/dist/reloads.js +7 -0
- package/dist/reloads.js.map +1 -0
- package/dist/runtime.d.ts +24 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +58 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schedule-triggers.d.ts +14 -10
- package/dist/schedule-triggers.d.ts.map +1 -1
- package/dist/schedule-triggers.js +36 -31
- package/dist/schedule-triggers.js.map +1 -1
- package/dist/scheduler.d.ts +45 -5
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +587 -44
- package/dist/scheduler.js.map +1 -1
- package/dist/ssh.d.ts +103 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +144 -0
- package/dist/ssh.js.map +1 -0
- package/dist/trigger-condition.d.ts +12 -0
- package/dist/trigger-condition.d.ts.map +1 -0
- package/dist/trigger-condition.js +88 -0
- package/dist/trigger-condition.js.map +1 -0
- package/dist/trigger-submission.d.ts +16 -13
- package/dist/trigger-submission.d.ts.map +1 -1
- package/dist/trigger-submission.js +19 -14
- package/dist/trigger-submission.js.map +1 -1
- package/dist/worker-engine.d.ts +13 -0
- package/dist/worker-engine.d.ts.map +1 -0
- package/dist/worker-engine.js +56 -0
- package/dist/worker-engine.js.map +1 -0
- package/dist/worker-status.d.ts +140 -0
- package/dist/worker-status.d.ts.map +1 -0
- package/dist/worker-status.js +76 -0
- package/dist/worker-status.js.map +1 -0
- package/package.json +47 -22
package/README.md
CHANGED
|
@@ -1,49 +1,102 @@
|
|
|
1
1
|
# @usine/orchestrator
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
0.x APIs can change between minor releases. Supply `effect` at the workspace's pinned version.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Operator
|
|
10
10
|
|
|
11
|
-
`Operator
|
|
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, last preflight per harness, and Attempt container limits (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
|
+
`Operator.statistics(instance, window)` and `Operator.runStatistics(instance, window)` cache their computed views in memory for up to five minutes, separately for `7d`, `30d`, and `all`. Concurrent requests for the same view and window share a computation. Each request checks the store's projection revision: run closure, retry, failure resolution, and close-time changes invalidate cached views, as do item and budget changes. Reloading the instance also invalidates them. If the revision record is missing, requests bypass the cache. Debug logs report cache hits and misses with the window.
|
|
18
|
+
|
|
19
|
+
`GET /api/statistics/runs` includes `needsAttention.failureWait`, the median time from failure to the first recorded operator answer. `retryWait` and `resolutionWait` report those answers separately. A resolution followed by a retry counts once, as a resolution. Unanswered failures and older resolutions without timestamps contribute no wait.
|
|
20
|
+
|
|
21
|
+
### Overview and board
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
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 item's current labels; if none matches, to the first label lane matching the labels in that run's event payload (`RunReader.events`), until the run ends; if neither matches, the item's state decides. `Operator.task` places tasks the same way, and `Operator.moveBoardItem` records the lane the card left (ADR 0024).
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
`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.
|
|
32
|
+
|
|
33
|
+
### Run detail
|
|
34
|
+
|
|
35
|
+
`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:
|
|
36
|
+
|
|
37
|
+
- the newest attempt's worker;
|
|
38
|
+
- `run.agentBrain`: the saved agent definition's harness, model, and effort, which Retry reuses, including any edit-and-retry amendment;
|
|
39
|
+
- `agentDefinitionChanged`: whether the loaded agent definition differs from the saved one (false if the agent is no longer loaded). Edit and retry takes the current loaded definition, even when the task and agent name stay the same;
|
|
40
|
+
- `RunDurationView`: ended attempts' time, the running one's start, and how many ended without a recorded end or transcript row;
|
|
41
|
+
- `RunUsageView`: each session's highest token report, summed per session, pool, and run, at the harness's price or, for a Codex session, at OpenAI's API rates for its token breakdown (`@usine/agents/openai-prices`), as an estimate on the Claude and ChatGPT plans (ADR 0013). A Codex model without rates stays unpriced, with one warning per model. 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.
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
### Task
|
|
46
|
+
|
|
47
|
+
`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`.
|
|
48
|
+
|
|
49
|
+
### GitHub changes
|
|
50
|
+
|
|
51
|
+
`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.
|
|
52
|
+
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
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`.
|
|
56
|
+
|
|
57
|
+
### Cycle budgets, run again, and failure resolution
|
|
58
|
+
|
|
59
|
+
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).
|
|
60
|
+
|
|
61
|
+
Each poll cycle closes the ask of an item that is closed, merged, or gone from GitHub with the reason `ItemClosed`, without a reset, so the ask leaves the overview and the item keeps its budget. A failed run on such an item stays in `ProjectView.runs` with `canResolveFailure`, but the overview lists no `RunFailureNeed` for it, and it doesn't count toward `ProjectView.status`, the card's `needsYou`, or the run the card shows (`BoardTaskView.run`). `RunDetailView.settledItemState` gives the item's state. A waiting run on a closed item is revoked when its loop's `if:` stops matching, as with a label change. The overview lists no `ApprovalNeed` or `RunFailureNeed` for a run in a repository that `usine.yaml` no longer lists, which has no `ProjectView`.
|
|
62
|
+
|
|
63
|
+
`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.
|
|
64
|
+
|
|
65
|
+
`Operator.resolveFailure(instance, runId, attemptId)` clears attention for the current failed attempt, at `POST /api/runs/:runId/resolve-failure` with `{ "attemptId": "..." }`. The resolution records its time and the instance's configured operator login. 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
66
|
|
|
13
67
|
## Operator HTTP
|
|
14
68
|
|
|
15
|
-
Import `FactoryOverview`, `RunView`, `RunDetailView`,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
69
|
+
Import `FactoryOverview`, `RunView`, `RunDetailView`, board views, operator needs, and transcript request types from `/operator`. The `Operator` service and its HTTP contract share these schemas.
|
|
70
|
+
|
|
71
|
+
`/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.
|
|
72
|
+
|
|
73
|
+
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
74
|
|
|
34
75
|
## Polling triggers
|
|
35
76
|
|
|
36
|
-
`PollingTriggers.poll(instance)`
|
|
77
|
+
`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.
|
|
78
|
+
|
|
79
|
+
`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.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
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
84
|
|
|
38
|
-
|
|
85
|
+
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
86
|
|
|
40
|
-
|
|
87
|
+
A pull request's mergeability changes when its base branch moves, without a change that detection lists. The cursor keeps each open pull request with its base branch, `mergeable`, and whether checks are unsettled, plus the head of each base branch. Each cycle reads every base head with `GitHubPolling.baseRefHead`, and fetches an open pull request again when its base head moved or can't be read, or while its `mergeable` is `UNKNOWN`. The first read of a head counts as moved.
|
|
41
88
|
|
|
42
|
-
Polling
|
|
89
|
+
Polling also fetches an open pull request on every cycle while its check rollup is `PENDING` or `EXPECTED`, or while the rollup is absent and `mergeStateStatus` is `BLOCKED`. An absent rollup on a blocked pull request conservatively means checks are expected, including when reviews also block merging. A terminal rollup stops these retries; an absent rollup with another merge state does not cause them. Each retry costs one snapshot request, uses the existing projection and loop evaluation path, and respects GitHub backoff. Older cursors refresh open pull requests once to learn their check state. A later check rerun on a settled pull request still needs an item update or base movement to trigger a fetch.
|
|
43
90
|
|
|
44
|
-
|
|
91
|
+
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.
|
|
45
92
|
|
|
46
|
-
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
`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
100
|
|
|
48
101
|
```ts
|
|
49
102
|
import { Layer } from "effect"
|
|
@@ -61,21 +114,48 @@ export const triggersLayer = PollingTriggers.layer.pipe(
|
|
|
61
114
|
)
|
|
62
115
|
```
|
|
63
116
|
|
|
64
|
-
Call `poll`
|
|
117
|
+
Call `poll` if another scheduler owns timing; `run` is the built-in endless loop.
|
|
65
118
|
|
|
66
119
|
## Schedule triggers
|
|
67
120
|
|
|
68
|
-
`ScheduleTriggers.tick(instance)` evaluates the
|
|
121
|
+
`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.
|
|
122
|
+
|
|
123
|
+
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.
|
|
69
124
|
|
|
70
|
-
|
|
125
|
+
`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`.
|
|
71
126
|
|
|
72
|
-
|
|
127
|
+
## Instance reload
|
|
128
|
+
|
|
129
|
+
`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.
|
|
130
|
+
|
|
131
|
+
`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.
|
|
132
|
+
|
|
133
|
+
`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
134
|
|
|
74
135
|
## Worker scheduling
|
|
75
136
|
|
|
76
|
-
|
|
137
|
+
`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 first waiting run in `leaseOrder` (`lease-order.ts`): a reservation naming a worker, then a run of a loop with `priority`, higher first, then a run by the lane its trigger matched, later lane first, then any other run, each group in event order. The position is read from the current loop file and the repository's `lanes` when the run starts waiting. Runs stay pending while all slots are taken. Each lease logs the run, its position and its source, and how many waiting runs with earlier events it passed.
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
With `options.machines` (`WorkerMachines`, which the SSH composition passes), a worker that creates a machine per attempt gets one before its lease: when a slot frees, the scheduler takes the next waiting run in lease order, creates the machine, and then leases the slot with it; the run's attempt gets the machine in `execute`, and the lease's release deletes it. A reservation naming a worker, a reattachment, creates no machine, and `occupy` holds the attempt's machine instead. A failed creation isn't an attempt failure: the slot frees, the run keeps its place in the queue, and the worker creates no machine for a pause of 30 seconds that doubles with each failure in a row, up to 10 minutes. A failed first-machine check is recorded as a preflight failure too. A run that stops waiting interrupts its creation, which deletes the machine. `StepExecutor.sweep` runs `Executor.sweep`, which removes the Codex homes that a crash left on SSH workers, then `WorkerMachines.sweep`.
|
|
142
|
+
|
|
143
|
+
`StepExecutor.discard` runs `options.discard`, or does nothing without it. The local, SSH, Cloudflare, and exe.dev compositions pass the `discard` of `sshWorkerRecordsFor` on `.usine/ssh-workers` in the instance directory, where the SSH executor records each long-lived SSH worker's machine (`withSshWorkerDiscard`). So **Discard leftovers** removes the discarded attempts' Codex homes from an SSH worker that `usine.yaml` no longer declares, even after it replaced the SSH workers with workers of another kind.
|
|
144
|
+
|
|
145
|
+
The layer registers with `CurrentInstance`, so a reload changes workers without touching running attempts:
|
|
146
|
+
|
|
147
|
+
- 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.
|
|
148
|
+
- 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.
|
|
149
|
+
- 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.
|
|
150
|
+
- 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`.
|
|
151
|
+
|
|
152
|
+
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.
|
|
77
153
|
|
|
78
|
-
|
|
154
|
+
The layer also provides `WorkerStatus` (`/worker-status`): each worker's slots and how many are taken, the waiting runs in lease order, and the last preflight of each worker's own prerequisites and of each harness, with time and result: passed, failed, for an SSH worker without Docker, degraded, or, for a provisioned worker's harnesses before its first machine of the day, deferred. With them, it keeps the limits of each attempt container from the last passed preflight that checked the worker's machine, which a provisioned worker's first machine of the day reports; a preflight without a machine keeps them. It also lists the provisioned workers' machines in flight and each provisioned worker's last failed creation with its next try. 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.
|
|
155
|
+
|
|
156
|
+
## Compositions
|
|
157
|
+
|
|
158
|
+
`/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
159
|
|
|
80
160
|
```ts
|
|
81
161
|
import { layer as localLayer } from "@usine/orchestrator/local"
|
|
@@ -85,11 +165,61 @@ const EngineLive = localLayer(instanceDirectory, config, brains, {
|
|
|
85
165
|
})
|
|
86
166
|
```
|
|
87
167
|
|
|
88
|
-
Provide `CredentialProvider` to `EngineLive`, then request `RunEngine
|
|
168
|
+
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.
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
`runtimeLayer(instanceDirectory, instanceId)` (`/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`, the Cloudflare composition if every worker is `cloudflare`, 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.
|
|
173
|
+
|
|
174
|
+
`/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, with their `container` settings, 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`. `workerEngineLayer` labels the SSH executor's attempt containers with `github-app-<App ID>` from `GitHubCredentials`, the instance's identity (ADR 0011), and with the `InstanceId` that the host passes, which tells apart instances that share the App.
|
|
175
|
+
|
|
176
|
+
`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.
|
|
177
|
+
|
|
178
|
+
`/cloudflare` composes the scheduler with the Cloudflare executor, with the GitHub App's ID as the instance that owns the sandboxes. `cloudflareWorkersOf(config)` returns the declared Cloudflare workers; `cloudflareWorkerAddressesOf(config)` turns them into executor addresses, reading each worker's Access service token and provider API keys from the variables that it names, and fails with a `credentials` capability error naming an unset one. The executor reads these secrets once, so a reload can resize or remove a Cloudflare worker, and add back only one that `usine up` started with; any other addition refuses the reload with `ReloadPreflightError`.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { InstanceId } from "@usine/core/instance"
|
|
182
|
+
import { layer as sshLayer, sshWorkerAddressesOf } from "@usine/orchestrator/ssh"
|
|
183
|
+
import { Effect, Layer } from "effect"
|
|
184
|
+
|
|
185
|
+
const EngineLive = Layer.unwrap(
|
|
186
|
+
Effect.map(sshWorkerAddressesOf(config), (workers) =>
|
|
187
|
+
sshLayer(instanceDirectory, config, brains, {
|
|
188
|
+
workers,
|
|
189
|
+
instance: "github-app-123456",
|
|
190
|
+
instanceId: InstanceId.make("0b4e7f3c-5a2d-4c1e-9f6b-8d7a2e1c3b4f"),
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
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
197
|
|
|
90
|
-
|
|
198
|
+
```sh
|
|
199
|
+
export USINE_SSH_INTEGRATION_DESTINATION=usine@203.0.113.7
|
|
200
|
+
export USINE_SSH_INTEGRATION_IDENTITY_FILE=~/.ssh/usine_worker # optional
|
|
201
|
+
export USINE_SSH_INTEGRATION_KNOWN_HOSTS=/path/to/known_hosts # optional
|
|
202
|
+
export USINE_SSH_INTEGRATION_ROOT=/home/usine/.usine # the default
|
|
203
|
+
export USINE_SSH_INTEGRATION_REPO=owner/repo
|
|
204
|
+
export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
|
|
205
|
+
pnpm --filter @usine/orchestrator test:ssh
|
|
206
|
+
```
|
|
91
207
|
|
|
92
|
-
|
|
208
|
+
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:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
export USINE_GATEWAY_INTEGRATION_DESTINATION=worker-one # a Host with ProxyJump through the gateway
|
|
212
|
+
export USINE_GATEWAY_INTEGRATION_SSH_CONFIG=/path/to/ssh_config # optional
|
|
213
|
+
export USINE_GATEWAY_INTEGRATION_ROOT=/home/usine/.usine # the default
|
|
214
|
+
export USINE_GATEWAY_INTEGRATION_DOMAIN=gw.internal # the default
|
|
215
|
+
export USINE_GATEWAY_INTEGRATION_GATEWAY=usine-gateway # the default: the Gateway's SSH destination
|
|
216
|
+
export USINE_GATEWAY_INTEGRATION_ADMIN_TOKEN=... # from /etc/usine-gateway/secrets.env
|
|
217
|
+
export USINE_GATEWAY_INTEGRATION_REPO=owner/repo
|
|
218
|
+
export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
|
|
219
|
+
pnpm --filter @usine/orchestrator test:ssh
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For exe.dev, `/exe-dev` keeps the engine and SQLite runtime local and runs the worker contract on the named VM:
|
|
93
223
|
|
|
94
224
|
```ts
|
|
95
225
|
import { RepositoryName } from "@usine/core/repository"
|
|
@@ -107,9 +237,9 @@ const EngineLive = exeDevLayer(instanceDirectory, config, brains, {
|
|
|
107
237
|
})
|
|
108
238
|
```
|
|
109
239
|
|
|
110
|
-
The
|
|
240
|
+
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
241
|
|
|
112
|
-
The live exe.dev
|
|
242
|
+
The live exe.dev test needs an existing baked VM and its GitHub integration:
|
|
113
243
|
|
|
114
244
|
```sh
|
|
115
245
|
export USINE_EXE_DEV_INTEGRATION_WORKER=worker-name
|
|
@@ -119,9 +249,9 @@ export USINE_EXE_DEV_INTEGRATION_GITHUB_INTEGRATION=integration-name
|
|
|
119
249
|
pnpm --filter @usine/orchestrator test:exe-dev
|
|
120
250
|
```
|
|
121
251
|
|
|
122
|
-
## Live GitHub
|
|
252
|
+
## Live GitHub tests
|
|
123
253
|
|
|
124
|
-
The opt-in integration test needs a disposable issue in a
|
|
254
|
+
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
255
|
|
|
126
256
|
```sh
|
|
127
257
|
export USINE_GITHUB_INTEGRATION_INSTANCE_DIRECTORY=/path/to/instance
|
|
@@ -130,7 +260,7 @@ export USINE_GITHUB_INTEGRATION_ISSUE_NUMBER=123
|
|
|
130
260
|
pnpm --filter @usine/orchestrator test:integration
|
|
131
261
|
```
|
|
132
262
|
|
|
133
|
-
The agent contract test
|
|
263
|
+
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
264
|
|
|
135
265
|
```sh
|
|
136
266
|
pnpm build
|
|
@@ -140,12 +270,4 @@ export USINE_AGENT_INTEGRATION_CHILD_MODEL=claude-model-id
|
|
|
140
270
|
pnpm --filter @usine/orchestrator test:integration:agent
|
|
141
271
|
```
|
|
142
272
|
|
|
143
|
-
|
|
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.
|
|
273
|
+
Cleanup removes its kick and result comments.
|
package/dist/board/items.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { GitHubLabelName } from "@usine/config/label";
|
|
2
|
-
import type
|
|
2
|
+
import { type LaneItemState } from "@usine/config/board";
|
|
3
3
|
import { ItemKey, ItemNumber } from "@usine/core/item";
|
|
4
|
-
import type
|
|
5
|
-
import { Effect } from "effect";
|
|
4
|
+
import { type ItemProjection, ItemProjectionReader } from "@usine/engine/item-projection";
|
|
5
|
+
import { Effect, Option, Schema } from "effect";
|
|
6
6
|
import { BoardProjectionError } from "../operator/board.ts";
|
|
7
|
+
import type { RunView } from "../operator/run.ts";
|
|
7
8
|
export interface ProjectedItem {
|
|
8
9
|
readonly itemKey: ItemKey;
|
|
9
10
|
readonly kind: "issue" | "pr";
|
|
@@ -13,6 +14,24 @@ export interface ProjectedItem {
|
|
|
13
14
|
readonly state: LaneItemState;
|
|
14
15
|
readonly linkedPullRequests: ReadonlyArray<string>;
|
|
15
16
|
readonly labels: ReadonlyArray<GitHubLabelName>;
|
|
17
|
+
/** Whether GitHub reports the pull request branch as conflicting with its base. */
|
|
18
|
+
readonly conflicting: boolean;
|
|
16
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns the labels in an Item `payload` of `kind`, such as the payload of a Run's Event. Fails if
|
|
22
|
+
* the payload has no labels of that shape.
|
|
23
|
+
*/
|
|
24
|
+
export declare const payloadLabels: (kind: ProjectedItem["kind"], payload: Schema.Json) => Effect.Effect<readonly (string & import("effect/Brand").Brand<"GitHubLabelName">)[], Schema.SchemaError, never>;
|
|
25
|
+
/** Returns the Board's view of one stored Item. An Item gone from GitHub counts as closed. */
|
|
17
26
|
export declare const projectedItem: (item: ItemProjection) => Effect.Effect<ProjectedItem, BoardProjectionError>;
|
|
27
|
+
/**
|
|
28
|
+
* Returns the state of the stored Item `itemKey` if it's closed, merged, or gone from GitHub.
|
|
29
|
+
* Returns `None` for an open Item, an Item that no trigger stored, or a payload that doesn't decode.
|
|
30
|
+
*/
|
|
31
|
+
export declare const settledItemState: (itemKey: string & import("effect/Brand").Brand<"ItemKey">) => Effect.Effect<Option.Option<"issue-closed" | "pr-closed" | "pr-merged">, import("@usine/engine/engine").EnginePersistenceError, ItemProjectionReader>;
|
|
32
|
+
/**
|
|
33
|
+
* Returns whether `run` needs the operator: it waits for approval, or its failure is unresolved and
|
|
34
|
+
* `itemSettled` is false. An Item is settled when it's closed, merged, or gone from GitHub.
|
|
35
|
+
*/
|
|
36
|
+
export declare const runNeedsAttention: (run: RunView, itemSettled: boolean) => boolean;
|
|
18
37
|
//# 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,
|
|
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,EAAsB,KAAK,aAAa,EAAyB,MAAM,qBAAqB,CAAC;AACpG,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,KAAK,cAAc,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1F,OAAO,EAAE,MAAM,EAAE,MAAM,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,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;AAEL;;;GAGG;AACH,eAAO,MAAM,gBAAgB,sNAe3B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAAS,OAAO,eAAe,OAAO,KAAG,OACZ,CAAC"}
|
package/dist/board/items.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { GitHubLabelName } from "@usine/config/label";
|
|
2
|
+
import { isSettledItemState } from "@usine/config/board";
|
|
2
3
|
import { ItemKey, ItemNumber } from "@usine/core/item";
|
|
4
|
+
import { ItemProjectionReader } from "@usine/engine/item-projection";
|
|
3
5
|
import { IssuePayload, PullRequestPayload } from "@usine/github/polling";
|
|
4
|
-
import { Effect, Schema } from "effect";
|
|
6
|
+
import { Effect, Option, Result, Schema } from "effect";
|
|
5
7
|
import { BoardProjectionError } from "../operator/board.js";
|
|
6
8
|
const labelsFrom = Schema.decodeUnknownEffect(Schema.Array(GitHubLabelName));
|
|
9
|
+
// Only the labels are read, so a payload stored before later fields were added still decodes.
|
|
10
|
+
const IssueLabels = Schema.Struct({ labels: IssuePayload.fields.labels });
|
|
11
|
+
const PullRequestLabels = Schema.Struct({ labels: PullRequestPayload.fields.labels });
|
|
12
|
+
/**
|
|
13
|
+
* Returns the labels in an Item `payload` of `kind`, such as the payload of a Run's Event. Fails if
|
|
14
|
+
* the payload has no labels of that shape.
|
|
15
|
+
*/
|
|
16
|
+
export const payloadLabels = (kind, payload) => Schema.decodeUnknownEffect(kind === "issue" ? IssueLabels : PullRequestLabels)(payload).pipe(Effect.flatMap(({ labels }) => labelsFrom(labels.nodes.map(({ name }) => name))));
|
|
7
17
|
const projectionError = (item, message) => new BoardProjectionError({
|
|
8
18
|
repository: item.repository,
|
|
9
19
|
itemKey: item.itemKey,
|
|
10
20
|
message,
|
|
11
21
|
});
|
|
22
|
+
/** Returns the Board's view of one stored Item. An Item gone from GitHub counts as closed. */
|
|
12
23
|
export const projectedItem = (item) => Effect.gen(function* () {
|
|
24
|
+
const gone = Option.isSome(item.goneAt);
|
|
13
25
|
if (item.source === "issue") {
|
|
14
26
|
const payload = yield* Schema.decodeUnknownEffect(IssuePayload)(item.payload).pipe(Effect.mapError((error) => projectionError(item, error.message)));
|
|
15
27
|
return {
|
|
@@ -18,16 +30,17 @@ export const projectedItem = (item) => Effect.gen(function* () {
|
|
|
18
30
|
number: ItemNumber.make(item.number),
|
|
19
31
|
title: payload.title,
|
|
20
32
|
url: payload.url,
|
|
21
|
-
state: payload.state === "OPEN" ? "issue-open" : "issue-closed",
|
|
33
|
+
state: payload.state === "OPEN" && !gone ? "issue-open" : "issue-closed",
|
|
22
34
|
linkedPullRequests: payload.closedByPullRequestsReferences.nodes.map(({ url }) => url),
|
|
23
35
|
labels: yield* labelsFrom(payload.labels.nodes.map(({ name }) => name)).pipe(Effect.mapError((error) => projectionError(item, error.message))),
|
|
36
|
+
conflicting: false,
|
|
24
37
|
};
|
|
25
38
|
}
|
|
26
39
|
if (item.source === "pr") {
|
|
27
40
|
const payload = yield* Schema.decodeUnknownEffect(PullRequestPayload)(item.payload).pipe(Effect.mapError((error) => projectionError(item, error.message)));
|
|
28
41
|
const state = payload.merged
|
|
29
42
|
? "pr-merged"
|
|
30
|
-
: payload.state === "OPEN"
|
|
43
|
+
: payload.state === "OPEN" && !gone
|
|
31
44
|
? "pr-open"
|
|
32
45
|
: "pr-closed";
|
|
33
46
|
return {
|
|
@@ -39,8 +52,35 @@ export const projectedItem = (item) => Effect.gen(function* () {
|
|
|
39
52
|
state,
|
|
40
53
|
linkedPullRequests: [],
|
|
41
54
|
labels: yield* labelsFrom(payload.labels.nodes.map(({ name }) => name)).pipe(Effect.mapError((error) => projectionError(item, error.message))),
|
|
55
|
+
conflicting: state === "pr-open" && payload.mergeable === "CONFLICTING",
|
|
42
56
|
};
|
|
43
57
|
}
|
|
44
|
-
return yield* projectionError(item, `Unsupported
|
|
58
|
+
return yield* projectionError(item, `Unsupported item projection source: ${item.source}`);
|
|
45
59
|
});
|
|
60
|
+
/**
|
|
61
|
+
* Returns the state of the stored Item `itemKey` if it's closed, merged, or gone from GitHub.
|
|
62
|
+
* Returns `None` for an open Item, an Item that no trigger stored, or a payload that doesn't decode.
|
|
63
|
+
*/
|
|
64
|
+
export const settledItemState = Effect.fn("settledItemState")(function* (itemKey) {
|
|
65
|
+
yield* Effect.annotateCurrentSpan({ itemKey });
|
|
66
|
+
const items = yield* ItemProjectionReader;
|
|
67
|
+
const stored = yield* items.get(itemKey);
|
|
68
|
+
if (Option.isNone(stored))
|
|
69
|
+
return Option.none();
|
|
70
|
+
const projected = yield* Effect.result(projectedItem(stored.value));
|
|
71
|
+
if (Result.isFailure(projected)) {
|
|
72
|
+
yield* Effect.logDebug("Stored Item payload doesn't decode; treating the Item as open", {
|
|
73
|
+
itemKey,
|
|
74
|
+
message: projected.failure.message,
|
|
75
|
+
});
|
|
76
|
+
return Option.none();
|
|
77
|
+
}
|
|
78
|
+
const { state } = projected.success;
|
|
79
|
+
return isSettledItemState(state) ? Option.some(state) : Option.none();
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Returns whether `run` needs the operator: it waits for approval, or its failure is unresolved and
|
|
83
|
+
* `itemSettled` is false. An Item is settled when it's closed, merged, or gone from GitHub.
|
|
84
|
+
*/
|
|
85
|
+
export const runNeedsAttention = (run, itemSettled) => run.canApprove || (run.canResolveFailure && !itemSettled);
|
|
46
86
|
//# sourceMappingURL=items.js.map
|
package/dist/board/items.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"items.js","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"items.js","sourceRoot":"","sources":["../../src/board/items.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,16 @@
|
|
|
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
|
-
|
|
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 goes to the label lane that its current Item's labels match. If none matches, the
|
|
12
|
+
* card stays in the lane its Run's trigger matched, read from `runEvents`.
|
|
13
|
+
*/
|
|
14
|
+
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
15
|
export declare const labelMutationForBoardMove: (board: BoardView, target: WritableBoardLaneView) => ItemLabelMutation;
|
|
9
16
|
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.d.ts","sourceRoot":"","sources":["../../src/board/projection.ts"],"names":[],"mappings":"
|
|
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;AAwHzD;;;;GAIG;AACH,eAAO,MAAM,YAAY,gmBAiFxB,CAAC;AAEF,eAAO,MAAM,yBAAyB,UAC7B,SAAS,UACR,qBAAqB,KAC5B,iBAaF,CAAC"}
|