@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.
Files changed (222) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +174 -52
  3. package/dist/board/items.d.ts +22 -3
  4. package/dist/board/items.d.ts.map +1 -1
  5. package/dist/board/items.js +44 -4
  6. package/dist/board/items.js.map +1 -1
  7. package/dist/board/projection.d.ts +8 -1
  8. package/dist/board/projection.d.ts.map +1 -1
  9. package/dist/board/projection.js +63 -14
  10. package/dist/board/projection.js.map +1 -1
  11. package/dist/board/tasks.d.ts +15 -3
  12. package/dist/board/tasks.d.ts.map +1 -1
  13. package/dist/board/tasks.js +28 -9
  14. package/dist/board/tasks.js.map +1 -1
  15. package/dist/cloudflare.d.ts +32 -0
  16. package/dist/cloudflare.d.ts.map +1 -0
  17. package/dist/cloudflare.js +68 -0
  18. package/dist/cloudflare.js.map +1 -0
  19. package/dist/credentials.d.ts +5 -6
  20. package/dist/credentials.d.ts.map +1 -1
  21. package/dist/credentials.js +22 -15
  22. package/dist/credentials.js.map +1 -1
  23. package/dist/current-instance/changes.d.ts +87 -0
  24. package/dist/current-instance/changes.d.ts.map +1 -0
  25. package/dist/current-instance/changes.js +141 -0
  26. package/dist/current-instance/changes.js.map +1 -0
  27. package/dist/current-instance/current-instance.d.ts +89 -0
  28. package/dist/current-instance/current-instance.d.ts.map +1 -0
  29. package/dist/current-instance/current-instance.js +163 -0
  30. package/dist/current-instance/current-instance.js.map +1 -0
  31. package/dist/exe-dev.d.ts +6 -2
  32. package/dist/exe-dev.d.ts.map +1 -1
  33. package/dist/exe-dev.js +10 -9
  34. package/dist/exe-dev.js.map +1 -1
  35. package/dist/http-client.d.ts +8 -0
  36. package/dist/http-client.d.ts.map +1 -0
  37. package/dist/http-client.js +8 -0
  38. package/dist/http-client.js.map +1 -0
  39. package/dist/idle-triggers.d.ts +1 -1
  40. package/dist/idle-triggers.d.ts.map +1 -1
  41. package/dist/idle-triggers.js +1 -1
  42. package/dist/idle-triggers.js.map +1 -1
  43. package/dist/label-reconciler.d.ts +5 -2
  44. package/dist/label-reconciler.d.ts.map +1 -1
  45. package/dist/label-reconciler.js.map +1 -1
  46. package/dist/lease-order.d.ts +60 -0
  47. package/dist/lease-order.d.ts.map +1 -0
  48. package/dist/lease-order.js +76 -0
  49. package/dist/lease-order.js.map +1 -0
  50. package/dist/local.d.ts +4 -18
  51. package/dist/local.d.ts.map +1 -1
  52. package/dist/local.js +9 -45
  53. package/dist/local.js.map +1 -1
  54. package/dist/loop-matches.d.ts +28 -0
  55. package/dist/loop-matches.d.ts.map +1 -0
  56. package/dist/loop-matches.js +74 -0
  57. package/dist/loop-matches.js.map +1 -0
  58. package/dist/operator/attempt.d.ts +76 -10
  59. package/dist/operator/attempt.d.ts.map +1 -1
  60. package/dist/operator/attempt.js +69 -11
  61. package/dist/operator/attempt.js.map +1 -1
  62. package/dist/operator/board.d.ts +48 -16
  63. package/dist/operator/board.d.ts.map +1 -1
  64. package/dist/operator/board.js +48 -17
  65. package/dist/operator/board.js.map +1 -1
  66. package/dist/operator/configuration.d.ts +314 -0
  67. package/dist/operator/configuration.d.ts.map +1 -0
  68. package/dist/operator/configuration.js +490 -0
  69. package/dist/operator/configuration.js.map +1 -0
  70. package/dist/operator/desk.d.ts +206 -0
  71. package/dist/operator/desk.d.ts.map +1 -0
  72. package/dist/operator/desk.js +525 -0
  73. package/dist/operator/desk.js.map +1 -0
  74. package/dist/operator/github-changes.d.ts +88 -0
  75. package/dist/operator/github-changes.d.ts.map +1 -0
  76. package/dist/operator/github-changes.js +266 -0
  77. package/dist/operator/github-changes.js.map +1 -0
  78. package/dist/operator/http.d.ts +135 -105
  79. package/dist/operator/http.d.ts.map +1 -1
  80. package/dist/operator/http.js +155 -30
  81. package/dist/operator/http.js.map +1 -1
  82. package/dist/operator/machines.d.ts +71 -0
  83. package/dist/operator/machines.d.ts.map +1 -0
  84. package/dist/operator/machines.js +103 -0
  85. package/dist/operator/machines.js.map +1 -0
  86. package/dist/operator/need.d.ts +58 -11
  87. package/dist/operator/need.d.ts.map +1 -1
  88. package/dist/operator/need.js +61 -9
  89. package/dist/operator/need.js.map +1 -1
  90. package/dist/operator/operator.d.ts +135 -29
  91. package/dist/operator/operator.d.ts.map +1 -1
  92. package/dist/operator/operator.js +370 -63
  93. package/dist/operator/operator.js.map +1 -1
  94. package/dist/operator/overview.d.ts +7 -7
  95. package/dist/operator/overview.d.ts.map +1 -1
  96. package/dist/operator/overview.js +5 -5
  97. package/dist/operator/overview.js.map +1 -1
  98. package/dist/operator/projection.d.ts +56 -5
  99. package/dist/operator/projection.d.ts.map +1 -1
  100. package/dist/operator/projection.js +108 -9
  101. package/dist/operator/projection.js.map +1 -1
  102. package/dist/operator/run.d.ts +101 -6
  103. package/dist/operator/run.d.ts.map +1 -1
  104. package/dist/operator/run.js +100 -7
  105. package/dist/operator/run.js.map +1 -1
  106. package/dist/operator/statistics/cache.d.ts +7 -0
  107. package/dist/operator/statistics/cache.d.ts.map +1 -0
  108. package/dist/operator/statistics/cache.js +40 -0
  109. package/dist/operator/statistics/cache.js.map +1 -0
  110. package/dist/operator/statistics/runs.d.ts +36 -0
  111. package/dist/operator/statistics/runs.d.ts.map +1 -0
  112. package/dist/operator/statistics/runs.js +284 -0
  113. package/dist/operator/statistics/runs.js.map +1 -0
  114. package/dist/operator/statistics/statistics.d.ts +20 -0
  115. package/dist/operator/statistics/statistics.d.ts.map +1 -0
  116. package/dist/operator/statistics/statistics.js +138 -0
  117. package/dist/operator/statistics/statistics.js.map +1 -0
  118. package/dist/operator/statistics/tasks.d.ts +30 -0
  119. package/dist/operator/statistics/tasks.d.ts.map +1 -0
  120. package/dist/operator/statistics/tasks.js +244 -0
  121. package/dist/operator/statistics/tasks.js.map +1 -0
  122. package/dist/operator/statistics/view.d.ts +302 -0
  123. package/dist/operator/statistics/view.d.ts.map +1 -0
  124. package/dist/operator/statistics/view.js +243 -0
  125. package/dist/operator/statistics/view.js.map +1 -0
  126. package/dist/operator/statistics/window.d.ts +24 -0
  127. package/dist/operator/statistics/window.d.ts.map +1 -0
  128. package/dist/operator/statistics/window.js +47 -0
  129. package/dist/operator/statistics/window.js.map +1 -0
  130. package/dist/operator/task.d.ts +94 -0
  131. package/dist/operator/task.d.ts.map +1 -0
  132. package/dist/operator/task.js +204 -0
  133. package/dist/operator/task.js.map +1 -0
  134. package/dist/operator/transcript.d.ts +12 -12
  135. package/dist/operator/transcript.d.ts.map +1 -1
  136. package/dist/operator/transcript.js +11 -9
  137. package/dist/operator/transcript.js.map +1 -1
  138. package/dist/operator/usage.d.ts +103 -0
  139. package/dist/operator/usage.d.ts.map +1 -0
  140. package/dist/operator/usage.js +234 -0
  141. package/dist/operator/usage.js.map +1 -0
  142. package/dist/orchestrator.d.ts +8 -5
  143. package/dist/orchestrator.d.ts.map +1 -1
  144. package/dist/orchestrator.js +4 -2
  145. package/dist/orchestrator.js.map +1 -1
  146. package/dist/polling/closed-item-asks.d.ts +8 -0
  147. package/dist/polling/closed-item-asks.d.ts.map +1 -0
  148. package/dist/polling/closed-item-asks.js +28 -0
  149. package/dist/polling/closed-item-asks.js.map +1 -0
  150. package/dist/polling/cycle.d.ts +28 -10
  151. package/dist/polling/cycle.d.ts.map +1 -1
  152. package/dist/polling/cycle.js +20 -6
  153. package/dist/polling/cycle.js.map +1 -1
  154. package/dist/polling/evaluation-failures.d.ts +42 -0
  155. package/dist/polling/evaluation-failures.d.ts.map +1 -0
  156. package/dist/polling/evaluation-failures.js +103 -0
  157. package/dist/polling/evaluation-failures.js.map +1 -0
  158. package/dist/polling/hints.d.ts +2 -2
  159. package/dist/polling/hints.d.ts.map +1 -1
  160. package/dist/polling/hints.js +2 -2
  161. package/dist/polling/hints.js.map +1 -1
  162. package/dist/polling/loop-evaluation.d.ts +21 -11
  163. package/dist/polling/loop-evaluation.d.ts.map +1 -1
  164. package/dist/polling/loop-evaluation.js +70 -27
  165. package/dist/polling/loop-evaluation.js.map +1 -1
  166. package/dist/polling/open-issues.d.ts +23 -0
  167. package/dist/polling/open-issues.d.ts.map +1 -0
  168. package/dist/polling/open-issues.js +77 -0
  169. package/dist/polling/open-issues.js.map +1 -0
  170. package/dist/polling/open-pull-requests.d.ts +28 -0
  171. package/dist/polling/open-pull-requests.d.ts.map +1 -0
  172. package/dist/polling/open-pull-requests.js +131 -0
  173. package/dist/polling/open-pull-requests.js.map +1 -0
  174. package/dist/polling/polling.d.ts +20 -11
  175. package/dist/polling/polling.d.ts.map +1 -1
  176. package/dist/polling/polling.js +215 -52
  177. package/dist/polling/polling.js.map +1 -1
  178. package/dist/polling/repository-sync.d.ts +21 -14
  179. package/dist/polling/repository-sync.d.ts.map +1 -1
  180. package/dist/polling/repository-sync.js +35 -28
  181. package/dist/polling/repository-sync.js.map +1 -1
  182. package/dist/polling-triggers.d.ts +1 -1
  183. package/dist/polling-triggers.d.ts.map +1 -1
  184. package/dist/polling-triggers.js +1 -1
  185. package/dist/polling-triggers.js.map +1 -1
  186. package/dist/reloads.d.ts +14 -0
  187. package/dist/reloads.d.ts.map +1 -0
  188. package/dist/reloads.js +7 -0
  189. package/dist/reloads.js.map +1 -0
  190. package/dist/runtime.d.ts +24 -0
  191. package/dist/runtime.d.ts.map +1 -0
  192. package/dist/runtime.js +58 -0
  193. package/dist/runtime.js.map +1 -0
  194. package/dist/schedule-triggers.d.ts +14 -10
  195. package/dist/schedule-triggers.d.ts.map +1 -1
  196. package/dist/schedule-triggers.js +36 -31
  197. package/dist/schedule-triggers.js.map +1 -1
  198. package/dist/scheduler.d.ts +45 -5
  199. package/dist/scheduler.d.ts.map +1 -1
  200. package/dist/scheduler.js +587 -44
  201. package/dist/scheduler.js.map +1 -1
  202. package/dist/ssh.d.ts +103 -0
  203. package/dist/ssh.d.ts.map +1 -0
  204. package/dist/ssh.js +144 -0
  205. package/dist/ssh.js.map +1 -0
  206. package/dist/trigger-condition.d.ts +12 -0
  207. package/dist/trigger-condition.d.ts.map +1 -0
  208. package/dist/trigger-condition.js +88 -0
  209. package/dist/trigger-condition.js.map +1 -0
  210. package/dist/trigger-submission.d.ts +16 -13
  211. package/dist/trigger-submission.d.ts.map +1 -1
  212. package/dist/trigger-submission.js +19 -14
  213. package/dist/trigger-submission.js.map +1 -1
  214. package/dist/worker-engine.d.ts +13 -0
  215. package/dist/worker-engine.d.ts.map +1 -0
  216. package/dist/worker-engine.js +56 -0
  217. package/dist/worker-engine.js.map +1 -0
  218. package/dist/worker-status.d.ts +140 -0
  219. package/dist/worker-status.d.ts.map +1 -0
  220. package/dist/worker-status.js +76 -0
  221. package/dist/worker-status.js.map +1 -0
  222. package/package.json +47 -22
@@ -1,35 +1,71 @@
1
1
  import { transcriptEvent } from "./projection.js";
2
2
  import { TranscriptSessionsQuery } from "@usine/engine/transcript";
3
3
  import { GitHubLabelName } from "@usine/config/label";
4
- import { resolveParentModel } from "@usine/core/agent";
5
- import { AttemptContext } from "@usine/core/attempt";
4
+ import { Agent, resolveParentModel } from "@usine/core/agent";
5
+ import { AttemptContext, AttemptId } from "@usine/core/attempt";
6
6
  import { Intervention } from "@usine/core/attempt/channel";
7
7
  import { Event, DedupKey, EventId } from "@usine/core/event";
8
8
  import { CycleBudgetAskId } from "@usine/core/cycle-budget";
9
+ import { BoardMoveRecord, BoardMoves } from "@usine/engine/board-moves";
9
10
  import { CycleBudgets } from "@usine/engine/cycle-budget";
10
11
  import { LoopName } from "@usine/core/loop";
11
12
  import { RunId } from "@usine/core/run";
12
- import { RunEngine, RunOperationRejectedError, } from "@usine/engine/engine";
13
+ import { ApprovalSource } from "@usine/core/run/approval";
14
+ import * as RunState from "@usine/core/run/state";
15
+ import { canonicalRepositoryName } from "@usine/core/repository";
16
+ import { RetryRequest, RunAgainRequest, RunEngine, RunOperationRejectedError, UnconfirmedStops, } from "@usine/engine/engine";
13
17
  import { ItemProjectionReader } from "@usine/engine/item-projection";
14
18
  import { LoopControl } from "@usine/engine/loop-control";
15
- import { RunReader } from "@usine/engine/run-reader";
19
+ import { RunReader, } from "@usine/engine/run-reader";
16
20
  import { RepositoryLabelProjectionReader } from "@usine/engine/repository-label-projection";
17
21
  import { GitHubItemWriter } from "@usine/github/item-writer";
18
22
  import { ChangedIssue, ChangedPullRequest } from "@usine/github/polling";
19
- import { Context, DateTime, Effect, Exit, Layer, Option, Schema, SynchronizedRef } from "effect";
23
+ import { Array as EffectArray, Context, DateTime, Effect, Exit, Layer, Match, Option, Schema, SynchronizedRef, } from "effect";
24
+ import { runNeedsAttention, settledItemState } from "../board/items.js";
20
25
  import { labelMutationForBoardMove, projectBoard } from "../board/projection.js";
26
+ import { LoopMatches } from "../loop-matches.js";
21
27
  import { GitHubItemKey, PollingTriggers } from "../polling-triggers.js";
22
28
  import { ScheduleTriggers } from "../schedule-triggers.js";
23
- import { ApprovalNeed, CycleBudgetNeed, BoardProjectionNeed, PollFailureNeed, PreflightFindingNeed, RunFailureNeed, ScheduleFailureNeed, } from "./need.js";
24
- import { BoardItemNotFoundError as BoardItemNotFound, BoardLaneNotFoundError as BoardLaneNotFound, BoardLaneReadOnlyError as BoardLaneReadOnly, BoardProjectNotFoundError as BoardProjectNotFound, LabelView, } from "./board.js";
29
+ import { cycleLimitOf } from "../trigger-submission.js";
30
+ import { ApprovalNeed, ConfigFileNeed, CycleBudgetNeed, BoardProjectionNeed, PollFailureNeed, PreflightFindingNeed, RunFailureNeed, ScheduleFailureNeed, UnconfiguredWorkerNeed, UnconfirmedStopNeed, } from "./need.js";
31
+ import { BoardItemNotFoundError as BoardItemNotFound, BoardLaneNotFoundError as BoardLaneNotFound, BoardLaneReadOnlyError as BoardLaneReadOnly, BoardProjectNotFoundError as BoardProjectNotFound, BoardTaskView, BoardView, LabelView, ReadOnlyBoardLaneView, WritableBoardLaneView, } from "./board.js";
25
32
  import { ColdTranscriptRequest, } from "./transcript.js";
26
33
  import { LoopView, ProjectView, ReadyOverview } from "./overview.js";
27
- import { RunDetailView, EditAndRetry, SessionIntervention, RosterAgentNotFoundError, } from "./run.js";
28
- import { readTranscript, runView } from "./projection.js";
29
- /** How many settled Runs the overview keeps beside the open ones. */
34
+ import { RunDetailView, EditAndRetry, LoopNotFoundError, SessionIntervention, RosterAgentNotFoundError, UnconfirmedStopView, } from "./run.js";
35
+ import { readRunningEvents, readTranscript, readUnrecordedEnds, runDuration, runView, workerAccesses, } from "./projection.js";
36
+ import { RunUsageReader, taskUsage } from "./usage.js";
37
+ import { RunGitHubChanges, } from "./github-changes.js";
38
+ import { TaskReader } from "./task.js";
39
+ import { OperatorDesk } from "./desk.js";
40
+ import { OperatorStatistics } from "./statistics/statistics.js";
41
+ import { machineView } from "./machines.js";
42
+ import { WorkerStatus } from "../worker-status.js";
43
+ /** The number of closed Runs the overview keeps next to the open ones. */
30
44
  const OVERVIEW_CLOSED_RUNS = 100;
31
- /** How many newest Items a Board shows; older ones drop off. */
45
+ /** The number of newest Items a Board shows. */
32
46
  const OVERVIEW_BOARD_ITEMS = 500;
47
+ const sameAgent = Schema.toEquivalence(Agent);
48
+ /**
49
+ * Returns `overview` with the usage of every card that contains a `changed` Run recomputed from
50
+ * `usage`.
51
+ */
52
+ const withRunUsage = (overview, changed, usage) => new ReadyOverview({
53
+ ...overview,
54
+ projects: overview.projects.map((project) => new ProjectView({
55
+ ...project,
56
+ board: new BoardView({
57
+ ...project.board,
58
+ lanes: project.board.lanes.map((lane) => {
59
+ const items = lane.items.map((task) => task.runs.some(({ id }) => changed.has(id))
60
+ ? new BoardTaskView({ ...task, usage: taskUsage(task.runs, usage) })
61
+ : task);
62
+ return lane._tag === "WritableBoardLane"
63
+ ? new WritableBoardLaneView({ ...lane, items })
64
+ : new ReadOnlyBoardLaneView({ ...lane, items });
65
+ }),
66
+ }),
67
+ })),
68
+ });
33
69
  const makeOperatorObservation = Effect.gen(function* () {
34
70
  const budgets = yield* CycleBudgets;
35
71
  const reader = yield* RunReader;
@@ -38,44 +74,90 @@ const makeOperatorObservation = Effect.gen(function* () {
38
74
  const repositoryLabels = yield* RepositoryLabelProjectionReader;
39
75
  const polling = yield* PollingTriggers;
40
76
  const schedules = yield* ScheduleTriggers;
77
+ const loopMatches = yield* LoopMatches;
41
78
  const cache = yield* SynchronizedRef.make(Option.none());
42
- const projectOverview = Effect.fn("Operator.projectOverview")(function* (instance, pollFailures, preflightFindings, scheduleFailures, revision) {
43
- const [snapshots, states, cycleAsks] = yield* Effect.all([
79
+ const usageReader = yield* RunUsageReader;
80
+ const stops = yield* UnconfirmedStops;
81
+ const workerStatus = yield* WorkerStatus;
82
+ const projectOverview = Effect.fn("Operator.projectOverview")(function* (instance, pollFailures, preflightFindings, scheduleFailures, revision, unconfirmedStops) {
83
+ const [snapshots, states, cycleAsks, workerAttempts] = yield* Effect.all([
44
84
  reader.list({ closedLimit: OVERVIEW_CLOSED_RUNS }),
45
85
  controls.states,
46
86
  budgets.asks(),
87
+ reader.attemptsByWorker(),
47
88
  ], { concurrency: "unbounded" });
48
89
  const paused = new Set(states.filter((state) => state.paused).map((state) => state.loop));
49
- const views = snapshots.map((snapshot) => runView(snapshot, paused));
90
+ const access = workerAccesses(instance.config.workers);
91
+ const views = snapshots.map((snapshot) => runView(snapshot, paused, access));
50
92
  const runsByRepository = new Map();
51
93
  for (const view of views) {
52
94
  const projectRuns = runsByRepository.get(view.repository) ?? [];
53
95
  projectRuns.push(view);
54
96
  runsByRepository.set(view.repository, projectRuns);
55
97
  }
56
- const projects = yield* Effect.forEach(instance.config.repos, (repository) => Effect.gen(function* () {
98
+ const projected = yield* Effect.forEach(instance.config.repos, (repository) => Effect.gen(function* () {
57
99
  const { name } = repository;
58
100
  const projectFindings = preflightFindings.filter(({ repository: findingRepository }) => findingRepository === name);
59
101
  const [boardItems, labelProjection] = yield* Effect.all([
60
102
  itemReader.list(name, { limit: OVERVIEW_BOARD_ITEMS }),
61
103
  repositoryLabels.get(name),
62
104
  ], { concurrency: "unbounded" });
63
- const history = yield* reader.forItems(boardItems.map(({ itemKey }) => itemKey));
105
+ const [history, matches] = yield* Effect.all([
106
+ reader.forItems(boardItems.map(({ itemKey }) => itemKey)),
107
+ loopMatches.matching(instance, repository, boardItems),
108
+ ], { concurrency: "unbounded" });
109
+ const itemLoops = new Map();
110
+ for (const { loop, item } of matches) {
111
+ itemLoops.set(item.itemKey, [...(itemLoops.get(item.itemKey) ?? []), loop.name]);
112
+ }
64
113
  const merged = new Map((runsByRepository.get(name) ?? []).map((run) => [run.id, run]));
65
114
  for (const saved of history) {
66
- const view = runView(saved, paused);
115
+ const view = runView(saved, paused, access);
67
116
  merged.set(view.id, view);
68
117
  }
69
118
  const projectRuns = [...merged.values()].sort((a, b) => b.openedAt.localeCompare(a.openedAt) || b.id.localeCompare(a.id));
119
+ // The reader keeps ended Attempts' reports, so only running Attempts are read again. A
120
+ // running Run's Event gives its card's lane while its Item has no lane label.
121
+ const [usages, runEvents] = yield* Effect.all([
122
+ Effect.forEach(history, (summary) => usageReader
123
+ .read(summary)
124
+ .pipe(Effect.map((usage) => [summary.run.id, usage])), { concurrency: 4 }),
125
+ readRunningEvents(reader, history),
126
+ ], { concurrency: "unbounded" });
127
+ const runUsage = new Map(usages);
128
+ yield* Effect.logDebug("Read the usage of the Board's Runs", {
129
+ repository: name,
130
+ runs: runUsage.size,
131
+ });
70
132
  const catalog = Option.match(labelProjection, {
71
133
  onNone: () => [],
72
134
  onSome: ({ labels }) => labels,
73
135
  });
74
- const board = yield* projectBoard(repository, catalog.map(({ name }) => name), boardItems, projectRuns, cycleAsks.map(({ itemKey }) => itemKey));
136
+ const board = yield* projectBoard(repository, catalog.map(({ name }) => name), boardItems, projectRuns, cycleAsks.map(({ itemKey }) => itemKey), itemLoops, runUsage, runEvents);
137
+ const working = history.filter(({ attempts }) => attempts.some(({ outcome }) => Option.isNone(outcome)));
138
+ return { name, projectFindings, projectRuns, board, catalog, runUsage, working };
139
+ }), { concurrency: 4 });
140
+ const allRuns = new Map(views.map((run) => [run.id, run]));
141
+ for (const { projectRuns } of projected)
142
+ for (const run of projectRuns)
143
+ allRuns.set(run.id, run);
144
+ // A Run of a repository that usine.yaml no longer lists has no Project to mark.
145
+ const managed = new Set(instance.config.repos.map(({ name }) => canonicalRepositoryName(name)));
146
+ const managedRuns = [];
147
+ for (const run of allRuns.values()) {
148
+ if (managed.has(canonicalRepositoryName(run.repository)))
149
+ managedRuns.push(run);
150
+ else if (run.state === "Waiting" || run.canResolveFailure)
151
+ yield* Effect.logDebug("Run's repository is not in usine.yaml; left out of Needs attention", { runId: run.id, repository: run.repository, state: run.state });
152
+ }
153
+ const failedItems = new Set(managedRuns.flatMap((run) => run.state !== "Waiting" && run.canResolveFailure ? [run.itemKey] : []));
154
+ // The Items of failed Runs that are closed, merged, or gone, with their state.
155
+ const settledItems = new Map(EffectArray.getSomes(yield* Effect.forEach(failedItems, (key) => settledItemState(key).pipe(Effect.provideService(ItemProjectionReader, itemReader), Effect.map(Option.map((state) => [key, state]))), { concurrency: 4 })));
156
+ const projects = projected.map(({ name, projectFindings, projectRuns, board, catalog }) => {
75
157
  const status = cycleAsks.some((ask) => ask.repository === name) ||
76
158
  board.failures.length > 0 ||
77
159
  projectFindings.length > 0 ||
78
- projectRuns.some((run) => run.state === "Waiting" || run.state === "Failed")
160
+ projectRuns.some((run) => runNeedsAttention(run, settledItems.has(run.itemKey)))
79
161
  ? "needs-you"
80
162
  : projectRuns.some((run) => run.canCancel)
81
163
  ? "active"
@@ -92,7 +174,11 @@ const makeOperatorObservation = Effect.gen(function* () {
92
174
  })),
93
175
  preflightFindings: projectFindings,
94
176
  });
95
- }), { concurrency: 4 });
177
+ });
178
+ const usage = {
179
+ runs: new Map(projected.flatMap(({ runUsage }) => [...runUsage])),
180
+ working: projected.flatMap(({ working }) => working),
181
+ };
96
182
  const loops = instance.loops.map((loop) => new LoopView({
97
183
  name: loop.name,
98
184
  trigger: loop.trigger.on,
@@ -115,23 +201,31 @@ const makeOperatorObservation = Effect.gen(function* () {
115
201
  }),
116
202
  }));
117
203
  }
118
- const allRuns = new Map(views.map((run) => [run.id, run]));
119
- for (const project of projects)
120
- for (const run of project.runs)
121
- allRuns.set(run.id, run);
122
- for (const run of allRuns.values()) {
204
+ const cardedItems = new Map(projects.flatMap(({ board }) => board.lanes.flatMap(({ items }) => items.flatMap((task) => task.items.map((item) => [item.itemKey, item])))));
205
+ for (const run of managedRuns) {
123
206
  if (run.state === "Waiting") {
124
207
  needsYou.push(new ApprovalNeed({
125
208
  id: `approval:${run.id}`,
126
209
  repository: run.repository,
127
210
  runId: run.id,
128
211
  loop: run.loop,
212
+ item: cardedItems.get(run.itemKey) ?? null,
129
213
  }));
130
214
  }
131
- else if (run.state === "Failed") {
215
+ else if (run.canResolveFailure) {
132
216
  const attempt = run.attempts.at(-1);
133
217
  if (attempt?.outcome?._tag !== "Failed") {
134
- return yield* Effect.die(new Error(`Failed Run ${run.id} has no failed Attempt`));
218
+ return yield* Effect.die(new Error(`Failed run ${run.id} has no failed attempt`));
219
+ }
220
+ const settled = settledItems.get(run.itemKey);
221
+ if (settled !== undefined) {
222
+ yield* Effect.logDebug("Failed Run's Item is closed; left out of Needs attention and the Project status", {
223
+ runId: run.id,
224
+ itemKey: run.itemKey,
225
+ itemState: settled,
226
+ failure: attempt.outcome.failure._tag,
227
+ });
228
+ continue;
135
229
  }
136
230
  needsYou.push(new RunFailureNeed({
137
231
  id: `run-failure:${run.id}`,
@@ -168,16 +262,78 @@ const makeOperatorObservation = Effect.gen(function* () {
168
262
  failure,
169
263
  }));
170
264
  });
171
- const projected = new ReadyOverview({ projects, loops, needsYou });
265
+ for (const { file, error } of instance.invalidAgents) {
266
+ needsYou.push(new ConfigFileNeed({ id: `config-file:${file}`, kind: "agent", error }));
267
+ }
268
+ for (const { file, error } of instance.quarantinedLoops) {
269
+ needsYou.push(new ConfigFileNeed({ id: `config-file:${file}`, kind: "loop", error }));
270
+ }
271
+ const configuredWorkers = new Set(instance.config.workers.map(({ name }) => name));
272
+ for (const { worker, attempts, running } of workerAttempts) {
273
+ if (configuredWorkers.has(worker))
274
+ continue;
275
+ needsYou.push(new UnconfiguredWorkerNeed({
276
+ id: `unconfigured-worker:${worker}`,
277
+ worker,
278
+ attempts,
279
+ running,
280
+ }));
281
+ }
282
+ for (const { attemptId, runId, worker } of unconfirmedStops) {
283
+ const run = allRuns.get(runId) ?? runView(yield* reader.inspect(runId), paused, access);
284
+ const attempt = run.attempts.find(({ id }) => id === attemptId);
285
+ if (attempt === undefined) {
286
+ yield* Effect.logWarning("The Run has no Attempt with the unconfirmed stop; no need shown", {
287
+ runId,
288
+ attemptId,
289
+ });
290
+ continue;
291
+ }
292
+ needsYou.push(new UnconfirmedStopNeed({
293
+ id: `unconfirmed-stop:${attemptId}`,
294
+ repository: run.repository,
295
+ runId,
296
+ loop: run.loop,
297
+ attemptNumber: attempt.number,
298
+ worker,
299
+ }));
300
+ }
301
+ const overview = new ReadyOverview({ projects, loops, needsYou });
172
302
  yield* Effect.logDebug("Refreshed operator overview", {
173
303
  revision,
174
304
  runs: views.length,
175
305
  needsYou: needsYou.length,
306
+ runsAtWork: usage.working.length,
307
+ });
308
+ return { overview, usage };
309
+ });
310
+ // Each session keeps its highest report, so a Run's usage changes only with more tokens or
311
+ // another session.
312
+ const refreshWorkingUsage = Effect.fn("Operator.refreshWorkingUsage")(function* (cached) {
313
+ if (cached.usage.working.length === 0)
314
+ return cached;
315
+ const read = yield* Effect.forEach(cached.usage.working, (summary) => usageReader.read(summary).pipe(Effect.map((usage) => [summary.run.id, usage])), { concurrency: 4 });
316
+ const changed = new Set(read.flatMap(([runId, usage]) => {
317
+ const before = cached.usage.runs.get(runId);
318
+ return before?.tokens === usage.tokens && before.sessions.length === usage.sessions.length
319
+ ? []
320
+ : [runId];
321
+ }));
322
+ if (changed.size === 0)
323
+ return cached;
324
+ const runs = new Map([...cached.usage.runs, ...read]);
325
+ yield* Effect.logDebug("Refreshed the usage of Runs at work on the cached overview", {
326
+ runsAtWork: read.length,
327
+ changed: changed.size,
176
328
  });
177
- return projected;
329
+ return {
330
+ ...cached,
331
+ overview: withRunUsage(cached.overview, changed, runs),
332
+ usage: { ...cached.usage, runs },
333
+ };
178
334
  });
179
335
  const overview = Effect.fn("Operator.overview")((instance) => SynchronizedRef.modifyEffect(cache, (cached) => Effect.gen(function* () {
180
- const [revision, latestPoll, latestSchedule] = yield* Effect.all([reader.revision(), polling.latest, schedules.latest], { concurrency: "unbounded" });
336
+ const [revision, latestPoll, latestSchedule, unconfirmedStops] = yield* Effect.all([reader.revision(), polling.latest, schedules.latest, stops.list], { concurrency: "unbounded" });
181
337
  const pollFailures = Option.match(latestPoll, {
182
338
  onNone: () => [],
183
339
  onSome: ({ failures }) => failures,
@@ -190,52 +346,119 @@ const makeOperatorObservation = Effect.gen(function* () {
190
346
  onNone: () => [],
191
347
  onSome: ({ failures }) => failures,
192
348
  });
193
- // Without the invalidation singleton staleness cannot be detected, so the
194
- // cache is bypassed.
349
+ // Without the invalidation singleton, staleness can't be detected, so the cache is
350
+ // bypassed.
195
351
  const token = Option.map(revision, (value) => JSON.stringify([
196
352
  value,
197
- instance.config.repos.map(({ name, lanes }) => [name, lanes]),
198
- instance.loops.map(({ name, agent, trigger }) => [name, agent, trigger.on]),
353
+ instance.config.repos.map(({ name, tags, lanes }) => [name, tags, lanes]),
354
+ [...workerAccesses(instance.config.workers)],
355
+ instance.loops.map(({ name, agent, trigger, selector }) => [
356
+ name,
357
+ agent,
358
+ trigger,
359
+ selector,
360
+ ]),
361
+ [...instance.invalidAgents, ...instance.quarantinedLoops].map(({ file, error }) => [
362
+ file,
363
+ error.stage,
364
+ error.message,
365
+ ]),
199
366
  pollFailures,
200
367
  preflightFindings,
201
368
  scheduleFailures,
369
+ unconfirmedStops.map(({ attemptId }) => attemptId),
202
370
  ]));
203
371
  if (Option.isNone(token)) {
204
372
  yield* Effect.logWarning("Projection revision singleton is missing; the overview cache is bypassed");
205
- const projected = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision);
373
+ const { overview: projected } = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision, unconfirmedStops);
206
374
  return [projected, Option.none()];
207
375
  }
208
376
  if (Option.isSome(cached) && cached.value.token === token.value) {
209
377
  yield* Effect.logDebug("Reusing unchanged operator overview", {
210
378
  revision,
211
379
  });
212
- return [cached.value.overview, cached];
380
+ const current = yield* refreshWorkingUsage(cached.value);
381
+ return [current.overview, Option.some(current)];
213
382
  }
214
- const projected = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision);
215
- return [projected, Option.some({ token: token.value, overview: projected })];
383
+ const projected = yield* projectOverview(instance, pollFailures, preflightFindings, scheduleFailures, revision, unconfirmedStops);
384
+ return [projected.overview, Option.some({ token: token.value, ...projected })];
216
385
  })));
217
- const detail = Effect.fn("Operator.detail")(function* (runId, request = new ColdTranscriptRequest()) {
386
+ /**
387
+ * Returns the unconfirmed stop on the Loop and Item of the open Run of `snapshot`, which keeps
388
+ * the Run from starting an Attempt, or null.
389
+ */
390
+ const blockingStop = Effect.fn("Operator.blockingStop")(function* (snapshot, unconfirmedStops) {
391
+ if (!RunState.isOpen(snapshot.run.state))
392
+ return null;
393
+ const stop = unconfirmedStops.find(({ loop, itemKey }) => loop === snapshot.run.loop && itemKey === snapshot.itemKey);
394
+ if (stop === undefined)
395
+ return null;
396
+ const fields = {
397
+ runId: snapshot.run.id,
398
+ unstoppedRunId: stop.runId,
399
+ attemptId: stop.attemptId,
400
+ };
401
+ yield* Effect.annotateCurrentSpan(fields);
402
+ const attempts = stop.runId === snapshot.run.id
403
+ ? snapshot.attempts
404
+ : (yield* reader.inspect(stop.runId)).attempts;
405
+ const attempt = attempts.find(({ id }) => id === stop.attemptId);
406
+ if (attempt === undefined) {
407
+ yield* Effect.logWarning("The Run with the unconfirmed stop has no such Attempt; the Run view shows no wait", fields);
408
+ return null;
409
+ }
410
+ return new UnconfirmedStopView({
411
+ runId: stop.runId,
412
+ attemptNumber: attempt.number,
413
+ worker: stop.worker,
414
+ });
415
+ });
416
+ const detail = Effect.fn("Operator.detail")(function* (instance, runId, request = new ColdTranscriptRequest()) {
218
417
  yield* Effect.annotateCurrentSpan({ runId });
219
- const [snapshot, states] = yield* Effect.all([reader.inspect(runId), controls.states], { concurrency: "unbounded" });
418
+ const [snapshot, states, unconfirmedStops, inFlight] = yield* Effect.all([reader.inspect(runId), controls.states, stops.list, workerStatus.machines], { concurrency: "unbounded" });
419
+ const openAttempt = snapshot.attempts.find((attempt) => Option.isNone(attempt.outcome));
420
+ // The machines are oldest first, so the last is the newest Attempt's.
421
+ const machine = [...inFlight]
422
+ .reverse()
423
+ .find((candidate) => candidate.state !== "deleting" &&
424
+ (Option.contains(candidate.runId, runId) ||
425
+ (openAttempt !== undefined && Option.contains(candidate.attemptId, openAttempt.id))));
220
426
  const paused = new Set(states.filter((state) => state.paused).map((state) => state.loop));
221
- const [transcript, sessionState] = yield* Effect.all([
427
+ const [transcript, sessionState, usage, unrecordedEnds, settled, unconfirmedStop] = yield* Effect.all([
222
428
  readTranscript(reader, snapshot, request),
223
429
  Effect.forEach(snapshot.attempts.filter((attempt) => Option.isNone(attempt.outcome)), (attempt) => reader.transcript(new TranscriptSessionsQuery({ attemptId: attempt.id })), { concurrency: "unbounded" }),
430
+ usageReader.read(snapshot),
431
+ readUnrecordedEnds(reader, snapshot),
432
+ settledItemState(snapshot.itemKey).pipe(Effect.provideService(ItemProjectionReader, itemReader)),
433
+ blockingStop(snapshot, unconfirmedStops),
224
434
  ], { concurrency: "unbounded" });
435
+ const currentAgent = instance.agents.find(({ name }) => name === snapshot.agent.name);
225
436
  return new RunDetailView({
226
- run: runView(snapshot, paused),
437
+ run: runView(snapshot, paused, workerAccesses(instance.config.workers)),
438
+ agentDefinitionChanged: currentAgent !== undefined && !sameAgent(snapshot.agent, currentAgent.agent),
227
439
  eventDedupKey: snapshot.event.dedupKey,
228
440
  eventPayload: snapshot.event.payload,
229
441
  transcript: transcript.events,
230
442
  sessionState: sessionState.flat().map(transcriptEvent),
231
443
  earlierTranscript: transcript.earlier,
444
+ worker: snapshot.attempts.at(-1)?.worker ?? null,
445
+ settledItemState: Option.getOrNull(settled),
446
+ duration: runDuration(snapshot.attempts, unrecordedEnds),
447
+ usage,
448
+ unconfirmedStop,
449
+ machine: machine === undefined ? null : machineView(machine),
232
450
  });
233
451
  });
234
- return { detail, overview };
452
+ const tasks = yield* TaskReader;
453
+ const { desk } = yield* OperatorDesk;
454
+ const { statistics, runStatistics } = yield* OperatorStatistics;
455
+ return { detail, overview, task: tasks.read, desk, statistics, runStatistics };
235
456
  });
236
- /** The read-only operator surface. It needs no trigger execution to build. */
457
+ /** The read-only operator surface, which needs no trigger execution. */
237
458
  export class OperatorObservation extends Context.Service()("@usine/orchestrator/OperatorObservation") {
238
- static layer = Layer.effect(OperatorObservation, makeOperatorObservation);
459
+ static layer = Layer.effect(OperatorObservation, makeOperatorObservation).pipe(Layer.provide(Layer.mergeAll(TaskReader.layer, OperatorDesk.layer, LoopMatches.layer, RunUsageReader.layer,
460
+ // Shares the overview's usage reader, so both read an ended Attempt's reports once.
461
+ OperatorStatistics.layer.pipe(Layer.provide(RunUsageReader.layer)))));
239
462
  }
240
463
  const makeOperator = Effect.gen(function* () {
241
464
  const budgets = yield* CycleBudgets;
@@ -245,8 +468,11 @@ const makeOperator = Effect.gen(function* () {
245
468
  const items = yield* ItemProjectionReader;
246
469
  const repositoryLabels = yield* RepositoryLabelProjectionReader;
247
470
  const github = yield* GitHubItemWriter;
471
+ const reader = yield* RunReader;
472
+ const boardMoves = yield* BoardMoves;
248
473
  const polling = yield* PollingTriggers;
249
- const approve = Effect.fn("Operator.approve")(function* (runId) {
474
+ const githubChanges = yield* RunGitHubChanges;
475
+ const approve = Effect.fn("Operator.approve")(function* (instance, runId) {
250
476
  yield* Effect.annotateCurrentSpan({ runId });
251
477
  const current = yield* runs.inspect(runId);
252
478
  if (current.run.state._tag !== "Waiting") {
@@ -267,12 +493,16 @@ const makeOperator = Effect.gen(function* () {
267
493
  dedupKey,
268
494
  payload: { runId, decision: "approved" },
269
495
  receivedAt,
270
- }));
496
+ }), new ApprovalSource({ channel: "ui", approver: instance.config.operator }));
271
497
  yield* Effect.logInfo("Operator approved Run", { runId });
272
498
  });
273
- const retry = Effect.fn("Operator.retry")(function* (runId) {
499
+ const resolveFailure = Effect.fn("Operator.resolveFailure")(function* (instance, runId, attemptId) {
500
+ yield* Effect.annotateCurrentSpan({ runId, attemptId });
501
+ yield* runs.resolveFailure(runId, attemptId, instance.config.operator);
502
+ });
503
+ const retry = Effect.fn("Operator.retry")(function* (instance, runId) {
274
504
  yield* Effect.annotateCurrentSpan({ runId });
275
- yield* runs.retry(runId);
505
+ yield* runs.retry(runId, new RetryRequest({ requestedBy: instance.config.operator, amendment: Option.none() }));
276
506
  yield* Effect.logInfo("Operator retried Run", { runId });
277
507
  });
278
508
  const editAndRetry = Effect.fn("Operator.editAndRetry")(function* (instance, runId, edit) {
@@ -281,16 +511,63 @@ const makeOperator = Effect.gen(function* () {
281
511
  if (file === undefined)
282
512
  return yield* new RosterAgentNotFoundError({
283
513
  agent: edit.agent,
284
- message: `Agent ${edit.agent} is not in the roster`,
514
+ message: `Agent "${edit.agent}" is not in the roster`,
285
515
  });
286
516
  const agent = file.agent;
287
- yield* runs.retry(runId, Option.some(new AttemptContext({
288
- task: edit.task,
289
- agent,
290
- parentModel: resolveParentModel(agent, instance.config.settings.parentModel),
291
- })));
517
+ yield* runs.retry(runId, new RetryRequest({
518
+ requestedBy: instance.config.operator,
519
+ amendment: Option.some(new AttemptContext({
520
+ task: edit.task,
521
+ agent,
522
+ parentModel: resolveParentModel(agent, instance.config.settings.parentModel),
523
+ })),
524
+ }));
292
525
  yield* Effect.logInfo("Operator amended and retried Run", { runId, agent: agent.name });
293
526
  });
527
+ const runAgain = Effect.fn("Operator.runAgain")(function* (instance, runId, edit) {
528
+ yield* Effect.annotateCurrentSpan({ runId, agent: edit.agent });
529
+ const current = yield* runs.inspect(runId);
530
+ const loop = instance.loops.find(({ name }) => name === current.run.loop);
531
+ if (loop === undefined) {
532
+ yield* Effect.logInfo("Run again rejected; the Run's Loop is not loaded", {
533
+ runId,
534
+ loop: current.run.loop,
535
+ });
536
+ return yield* new LoopNotFoundError({
537
+ loop: current.run.loop,
538
+ message: `Run ${runId} cannot run again: loop "${current.run.loop}" is not loaded.`,
539
+ });
540
+ }
541
+ const file = instance.agents.find(({ name }) => name === edit.agent);
542
+ if (file === undefined) {
543
+ yield* Effect.logInfo("Run again rejected; the Agent is not in the roster", {
544
+ runId,
545
+ agent: edit.agent,
546
+ });
547
+ return yield* new RosterAgentNotFoundError({
548
+ agent: edit.agent,
549
+ message: `Agent "${edit.agent}" is not in the roster`,
550
+ });
551
+ }
552
+ const agent = file.agent;
553
+ const result = yield* runs.runAgain(runId, new RunAgainRequest({
554
+ step: loop.step,
555
+ approval: loop.approval,
556
+ cycleLimit: cycleLimitOf(loop),
557
+ permissions: loop.permissions,
558
+ context: new AttemptContext({
559
+ task: edit.task,
560
+ agent,
561
+ parentModel: resolveParentModel(agent, instance.config.settings.parentModel),
562
+ }),
563
+ requestedBy: instance.config.operator,
564
+ }));
565
+ yield* Effect.logInfo("Operator ran a Run again", Match.valueTags(result, {
566
+ Claimed: ({ runId: openedRunId }) => ({ runId, agent: agent.name, openedRunId }),
567
+ CycleBlocked: ({ ask }) => ({ runId, agent: agent.name, askId: ask.id }),
568
+ }));
569
+ return result;
570
+ });
294
571
  const intervene = Effect.fn("Operator.intervene")(function* (instance, runId, input) {
295
572
  yield* Effect.annotateCurrentSpan({
296
573
  runId,
@@ -330,10 +607,16 @@ const makeOperator = Effect.gen(function* () {
330
607
  items.list(move.repository, { limit: OVERVIEW_BOARD_ITEMS }),
331
608
  repositoryLabels.get(move.repository),
332
609
  ], { concurrency: "unbounded" });
610
+ // Place each card as the Board does, so the move records the lane that the card left.
611
+ const summaries = yield* reader.forItems(storedItems.map(({ itemKey }) => itemKey));
612
+ const runEvents = yield* readRunningEvents(reader, summaries);
613
+ const access = workerAccesses(instance.config.workers);
333
614
  const board = yield* projectBoard(repository, Option.match(labelProjection, {
334
615
  onNone: () => [],
335
616
  onSome: ({ labels }) => labels.map(({ name }) => name),
336
- }), storedItems, []);
617
+ }), storedItems,
618
+ // Placement reads no pause state, so these views leave every Loop unpaused.
619
+ summaries.map((summary) => runView(summary, new Set(), access)), [], new Map(), new Map(), runEvents);
337
620
  const source = board.lanes.find(({ items: laneItems }) => laneItems.some(({ items }) => items.some(({ number }) => number === move.item)));
338
621
  const movedItem = storedItems.find(({ number }) => number === move.item);
339
622
  if (source === undefined || movedItem === undefined) {
@@ -347,7 +630,18 @@ const makeOperator = Effect.gen(function* () {
347
630
  return yield* new BoardLaneReadOnly({ repository: move.repository, lane: move.toLane });
348
631
  }
349
632
  const mutation = labelMutationForBoardMove(board, target);
350
- const changed = { number: move.item, updatedAt: yield* DateTime.now };
633
+ const movedAt = yield* DateTime.now;
634
+ // Recorded before the write, so the Run reader never attributes the swap to an Agent.
635
+ yield* boardMoves.record(new BoardMoveRecord({
636
+ repository: move.repository,
637
+ itemKey: movedItem.itemKey,
638
+ at: movedAt,
639
+ fromLane: source.name,
640
+ toLane: target.name,
641
+ added: mutation.add,
642
+ removed: mutation.remove,
643
+ }));
644
+ const changed = { number: move.item, updatedAt: movedAt };
351
645
  const item = movedItem.source === "pr" ? new ChangedPullRequest(changed) : new ChangedIssue(changed);
352
646
  const applied = yield* github.updateLabels(move.repository, move.item, mutation).pipe(
353
647
  // A failed or interrupted request may have applied part of the label
@@ -380,23 +674,36 @@ const makeOperator = Effect.gen(function* () {
380
674
  ...observation,
381
675
  approve,
382
676
  cancel,
677
+ discardLeftovers: (worker) => runs.discardLeftovers(worker).pipe(Effect.asVoid),
383
678
  moveBoardItem,
679
+ githubChanges: githubChanges.read,
384
680
  pause,
385
681
  retry,
682
+ resolveFailure,
386
683
  editAndRetry,
684
+ runAgain,
387
685
  intervene,
388
686
  unpause,
389
687
  resetCycleBudget,
390
688
  };
391
689
  });
392
- /** The operator's view of the factory and actions for approving, retrying, canceling, and directing work. */
690
+ /**
691
+ * The operator's factory view and actions for approving, retrying, canceling, and directing work.
692
+ */
393
693
  export class Operator extends Context.Service()("@usine/orchestrator/Operator") {
394
- static layer = Layer.effect(Operator, makeOperator).pipe(Layer.provide(OperatorObservation.layer));
694
+ static layer = Layer.effect(Operator, makeOperator).pipe(Layer.provide(Layer.merge(OperatorObservation.layer, RunGitHubChanges.layer)));
395
695
  }
396
696
  export { ProjectStatus, ProjectView, LoopView, ReadyOverview, NeedsSetupOverview, FactoryOverview, } from "./overview.js";
397
- export { RunStateTag, RunView, RunDetailView, EditAndRetry, SessionIntervention, RosterAgentNotFoundError, } from "./run.js";
398
- export { InfraFailureView, DefectFailureView, AgentFailureView, RunawayFailureView, ChildSessionBudgetFailureView, FailureView, SucceededAttemptView, FailedAttemptView, CanceledAttemptView, AttemptOutcomeView, AttemptView, } from "./attempt.js";
399
- export { BoardLaneId, BoardLaneOrigin, BoardItemLink, BoardTaskView, WritableBoardLaneView, ReadOnlyBoardLaneView, BoardLaneView, BoardProjectionError, BoardView, LabelView, BoardMove, BoardProjectNotFoundError, BoardItemNotFoundError, BoardLaneNotFoundError, BoardLaneReadOnlyError, } from "./board.js";
400
- export { ApprovalNeed, CycleBudgetNeed, RunFailureNeed, PollFailureNeed, PreflightFindingNeed, BoardProjectionNeed, ScheduleFailureNeed, NeedView, } from "./need.js";
697
+ export { LabelAddedView, LabelRemovedView, CommentedView, ReviewedView, BoardMovedView, GitHubChangeView, AttemptGitHubChangesView, RunGitHubChangesView, } from "./github-changes.js";
698
+ export { ApprovalView, RunStateTag, RunView, RunDurationView, RunDetailView, UnconfirmedStopView, EditAndRetry, LoopNotFoundError, SessionIntervention, RosterAgentNotFoundError, } from "./run.js";
699
+ export { UsageCostView, SessionUsageView, PoolUsageView, RunUsageView, TaskPoolUsageView, TaskUsageView, } from "./usage.js";
700
+ export { InfraFailureView, DefectFailureView, AgentFailureView, BlockedFailureView, QuestionFailureView, RunawayFailureView, ChildSessionBudgetFailureView, FailureView, SucceededAttemptView, FailedAttemptView, CanceledAttemptView, AttemptOutcomeView, LocalWorkerAccess, SshWorkerAccess, ProvisionedWorkerAccess, SandboxWorkerAccess, WorkerAccess, AttemptView, } from "./attempt.js";
701
+ export { MachineView, ProvisioningPauseView, HetznerProvisionerView, BoatProvisionerView, ProvisionerView, } from "./machines.js";
702
+ export { BoardLaneId, BoardLaneOrigin, BoardItemLink, BoardPullRequestView, BoardTaskView, WritableBoardLaneView, ReadOnlyBoardLaneView, BoardLaneView, BoardProjectionError, BoardView, LabelView, BoardMove, BoardProjectNotFoundError, BoardItemNotFoundError, BoardLaneNotFoundError, BoardLaneReadOnlyError, } from "./board.js";
703
+ export { TaskItemView, TaskLaneView, TaskCommentView, TaskView, } from "./task.js";
704
+ export { ApprovalNeed, CycleBudgetNeed, RunFailureNeed, PollFailureNeed, PreflightFindingNeed, BoardProjectionNeed, ScheduleFailureNeed, ConfigFileNeed, UnconfiguredWorkerNeed, UnconfirmedStopNeed, NeedView, } from "./need.js";
705
+ export { LoopSelectorView, LoopFileView, QuarantinedLoopView, AgentFileView, InvalidAgentView, GatewayView, WorkerSlotsView, PassedCheckView, DegradedCheckView, DeferredCheckView, FailedCheckView, PreflightCheckView, ContainerView, HarnessPreflightView, WorkerConfigurationView, RemovedWorkerView, LabelStateView, LabelLaneSourceView, StateLaneSourceView, LaneSourceView, LaneConfigurationView, RepositoryConfigurationView, SettingsView, ConfigurationView, OperatorConfiguration, } from "./configuration.js";
706
+ export { WorkerSilentView, ProviderRetryingView, ConnectivityWarningView, RunningAttemptView, SuppressedForOperatorView, ClosedUnchangedView, RunFailedView, LoopPausedView, WaitingForStopView, CycleBudgetUsedUpView, AwaitingApprovalView, QueuedForWorkerView, AwaitingPollView, StallReasonView, StalledItemView, ClosedRunView, WorkerView, RepositoryPollView, DeskView, } from "./desk.js";
707
+ export { StatisticsWindow, StatisticsBucket, TaskDoneView, TaskPeriodView, LoopShareView, LoopRunTimeView, TaskTimeView, TaskCostView, TaskTokensView, UntimedTasksView, StatisticsView, DurationSummaryView, OutcomeCountsView, PoolTokensView, CostSummaryView, UsageSummaryView, LoopStatisticsView, AgentStatisticsView, BrainStatisticsView, ThroughputPeriodView, WorkerQueueWaitView, AttemptCountView, WorkerUtilizationView, NeedsAttentionPeriodView, NeedsAttentionView, RunStatisticsView, } from "./statistics/view.js";
401
708
  export { FailedTranscriptView, TranscriptPayload, TranscriptEventView, TranscriptPageCursor, ColdTranscriptRequest, TailTranscriptRequest, EarlierTranscriptRequest, TranscriptRequest, TranscriptAttemptNotFoundError, } from "./transcript.js";
402
709
  //# sourceMappingURL=operator.js.map