@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

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 (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -1,23 +1,27 @@
1
- import { basename, dirname } from 'node:path'
2
-
3
- import has from 'lodash/has'
4
- import partition from 'lodash/partition'
5
-
6
- import { decideSteps, type EngineAction } from './decide'
7
- import { isFinalReviewAction } from './decide-final-review'
8
- import { isMemoryAction } from './decide-memory'
9
- import type { PlanAction } from './decide-plan'
10
- import type { UsageLineAction } from './decide-usage-line'
11
- import { executeAlreadyDoneAction } from './execute-already-done'
1
+ /**
2
+ * `runEngine`: the scheduler that runs the decision step's actions, tickets'
3
+ * steps at the same time. Re-exports the run's first records and
4
+ * `executeAction` from the modules that carry them out.
5
+ */
6
+ import { decideNext, type EngineAction } from './decide'
7
+ import { repoWaitUnchecked } from './decide-repo-wait'
8
+ import { executeAction, type StepTry } from './execute-action'
9
+ import type { BuildDeps } from './execute-build'
10
+ import { readEarlierRuns, type GoingRunsReader } from './execute-repo-wait'
11
+ import { executeWithJev } from './execute-with-jev'
12
+ import { createLiveFile } from './live-file'
12
13
  import {
13
- buildContext,
14
- executeBuildAction,
15
- type BuildDeps,
16
- } from './execute-build'
17
- import { writeChangeset } from './execute-changeset'
18
- import { executeFinalReviewAction } from './execute-final-review'
19
- import { executeMemoryAction } from './execute-memory'
20
- import { executeStuckAction } from './execute-stuck'
14
+ agentRoleOf,
15
+ crashesIn,
16
+ followUpSessionOf,
17
+ keyOf,
18
+ redosFirst,
19
+ SLOW_ACTIONS,
20
+ stepOf,
21
+ ticketOf,
22
+ usesRunBranch,
23
+ WAIT_ACTIONS,
24
+ } from './schedule-rules'
21
25
  import {
22
26
  closeSessions,
23
27
  finishedSessions,
@@ -25,45 +29,24 @@ import {
25
29
  } from './session-close'
26
30
 
27
31
  import type { BoardSync } from '../board/board-sync'
28
- import type { EngineConfig } from '../config/engine-config'
29
- import { lucaVersion } from '../config/luca-version'
30
- import { outsideBlockerNumbers } from '../intake/intake-checks'
31
- import type { IntakeProblem } from '../intake/intake-schemas'
32
- import { jevAsksAfter, jevAsksBefore } from '../jev/jev-jobs'
33
- import { askJevInShadow, type JevShadow } from '../jev/jev-shadow'
32
+ import { hearSlotWaits } from '../gates/slot-waits'
33
+ import { shadowAsker, type JevShadow } from '../jev/jev-shadow'
34
34
  import type { Journal } from '../journal/journal'
35
35
  import type { JournalRecord } from '../journal/journal-record'
36
- import { replayRun } from '../journal/replay'
37
36
  import {
38
- crashesAfter,
39
37
  resumeEntry,
40
38
  stepFirstSeq,
41
39
  type CrashCounts,
42
40
  } from '../journal/step-records'
43
- import {
44
- LIMIT_WAIT_NAP_MS,
45
- limitWaitComment,
46
- SYSTEM_CLOCK,
47
- waitUntil,
48
- type EngineClock,
49
- } from '../limits/limit-wait'
50
- import {
51
- DEFAULT_USAGE_LINES,
52
- LINE_OF,
53
- runReadings,
54
- usageLineEndedComment,
55
- usageLineWaitComment,
56
- type UsageLineDeps,
57
- type UsageLines,
58
- } from '../limits/usage-line'
41
+ import type { EngineClock } from '../limits/limit-wait'
42
+ import { runReadings, type UsageLineDeps } from '../limits/usage-line'
59
43
  import type { MemoryDeps } from '../memory/memory-client'
60
- import { postCommentOnce, type CommentStep } from '../tracker/post-comment-once'
61
- import {
62
- NEEDS_INFO_LABEL,
63
- READY_LABEL,
64
- type Tracker,
65
- type TrackerIssue,
66
- } from '../tracker/tracker'
44
+ import type { Tracker } from '../tracker/tracker'
45
+
46
+ export { executeAction, type StepTry } from './execute-action'
47
+ export { refusalComment } from './execute-intake'
48
+ export { recordConfigReload, recordResume, startRun } from './run-start'
49
+ export { keyOf, stepOf } from './schedule-rules'
67
50
 
68
51
  /** How many actions `runEngine` takes before it gives up, as a safety net. */
69
52
  export const DEFAULT_MAX_STEPS = 1000
@@ -74,770 +57,9 @@ export const STOP_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
74
57
  'invalid_journal',
75
58
  ])
76
59
 
77
- /**
78
- * Starts a run by writing its first record. The spec and config it names are
79
- * what every later step of the run works from; the Luca version it names is
80
- * the one the run started on.
81
- */
82
- export const startRun = ({
83
- journal,
84
- spec_number,
85
- config,
86
- base_branch,
87
- memory,
88
- repo,
89
- luca_version = lucaVersion(),
90
- }: {
91
- journal: Journal
92
- spec_number: number
93
- config: EngineConfig
94
- /** The branch the run branch starts from. Defaults to `main`. */
95
- base_branch?: string
96
- /**
97
- * Turns memory on (#370), with the project's vault (`null` searches only
98
- * `default`). Leave it out for a run without memory.
99
- */
100
- memory?: { project_vault: string | null }
101
- /** The repo the run is on, so a resume can find it. Defaults to `null`. */
102
- repo?: string | null
103
- /** Luca's version. Defaults to the engine's own. */
104
- luca_version?: string
105
- }): JournalRecord =>
106
- journal.append({
107
- kind: 'run_started',
108
- ticket: null,
109
- role: null,
110
- content: {
111
- spec_number,
112
- config,
113
- base_branch,
114
- memory: memory ?? null,
115
- repo,
116
- luca_version,
117
- },
118
- })
119
-
120
- /**
121
- * Records that the engine started again on a run's journal, on Luca's
122
- * version `luca_version`, before its first step.
123
- */
124
- export const recordResume = ({
125
- journal,
126
- luca_version = lucaVersion(),
127
- }: {
128
- journal: Journal
129
- /** Luca's version. Defaults to the engine's own. */
130
- luca_version?: string
131
- }): JournalRecord =>
132
- journal.append({
133
- kind: 'engine_resumed',
134
- ticket: null,
135
- role: null,
136
- content: { luca_version },
137
- })
138
-
139
- /** The comment a bad spec or ticket gets when intake refuses the run. */
140
- export const refusalComment = ({
141
- spec_number,
142
- missing,
143
- }: {
144
- spec_number: number
145
- missing: string[]
146
- }): string =>
147
- `Luca intake refused the run for spec #${spec_number}. This issue is not ready yet:\n\n` +
148
- `${missing.map((line) => `- ${line}`).join('\n')}\n\n` +
149
- `Fix these, move the issue back to \`${READY_LABEL}\`, and start the run again.`
150
-
151
- const readIntake = async ({
152
- spec_number,
153
- journal,
154
- tracker,
155
- }: {
156
- spec_number: number
157
- journal: Journal
158
- tracker: Tracker
159
- }) => {
160
- const spec = await tracker.readSpec({ spec_number })
161
- const subTickets = await tracker.listSubTickets({ spec_number })
162
- const outside = await Promise.all(
163
- outsideBlockerNumbers({ sub_tickets: subTickets }).map((number) =>
164
- tracker.readIssue({ number })
165
- )
166
- )
167
- journal.append({
168
- kind: 'intake_read',
169
- ticket: null,
170
- role: null,
171
- content: {
172
- spec,
173
- sub_tickets: subTickets,
174
- outside_blockers: outside.filter(
175
- (issue): issue is TrackerIssue => issue !== null
176
- ),
177
- },
178
- })
179
- }
180
-
181
- const refuseIntake = async ({
182
- spec_number,
183
- problems,
184
- journal,
185
- tracker,
186
- step,
187
- }: {
188
- spec_number: number
189
- problems: IntakeProblem[]
190
- journal: Journal
191
- tracker: Tracker
192
- step: CommentStep
193
- }) => {
194
- for (const [n, { ticket, missing }] of problems.entries()) {
195
- if (ticket === null) continue
196
- await postCommentOnce({
197
- tracker,
198
- number: ticket,
199
- body: refusalComment({ spec_number, missing }),
200
- step,
201
- n,
202
- })
203
- await tracker.addLabel({ number: ticket, label: NEEDS_INFO_LABEL })
204
- await tracker.removeLabel({ number: ticket, label: READY_LABEL })
205
- }
206
- journal.append({
207
- kind: 'intake_refused',
208
- ticket: null,
209
- role: null,
210
- content: { problems },
211
- })
212
- }
213
-
214
- /**
215
- * Carries out a limit wait or a billing stop. A limit wait's start tells the
216
- * spec issue (unless it already heard of this reset) before it is journaled;
217
- * the wait itself sleeps by `clock` until its end, so a restarted engine
218
- * waits out only what is left.
219
- */
220
- const executePlanAction = async ({
221
- action,
222
- journal,
223
- tracker,
224
- clock,
225
- step,
226
- }: {
227
- action: Exclude<PlanAction, { type: 'done' }>
228
- journal: Journal
229
- tracker: Tracker
230
- clock: EngineClock
231
- step: CommentStep
232
- }): Promise<void> => {
233
- switch (action.type) {
234
- case 'start_limit_wait': {
235
- const { rate_limit_type, resets_at, until, ticket, role } = action
236
- if (action.announce) {
237
- await postCommentOnce({
238
- tracker,
239
- number: action.spec_number,
240
- body: limitWaitComment({
241
- rate_limit_type,
242
- resets_at,
243
- until,
244
- }),
245
- step,
246
- n: 0,
247
- })
248
- }
249
- journal.append({
250
- kind: 'limit_wait_started',
251
- ticket: null,
252
- role: null,
253
- content: {
254
- resets_at,
255
- until,
256
- rate_limit_type,
257
- hit_ticket: ticket,
258
- hit_role: role,
259
- },
260
- })
261
- return
262
- }
263
- case 'wait_for_limit':
264
- await waitUntil({ clock, until: action.until })
265
- journal.append({
266
- kind: 'limit_wait_ended',
267
- ticket: null,
268
- role: null,
269
- content: { until: action.until },
270
- })
271
- return
272
- case 'stop_for_billing':
273
- journal.append({
274
- kind: 'run_stopped',
275
- ticket: null,
276
- role: null,
277
- content: { reason: action.reason, role: null, billing: true },
278
- })
279
- return
280
- }
281
- }
282
-
283
- /**
284
- * Carries out a usage-line wait. Its start tells the spec issue, then is
285
- * journaled. The wait sleeps by `clock` in naps until its end, re-reading
286
- * the lines before each nap: a line raised above the reading ends it at
287
- * once. Its end tells the spec issue why the run carries on, then is
288
- * journaled.
289
- */
290
- const executeUsageLineAction = async ({
291
- action,
292
- journal,
293
- tracker,
294
- clock,
295
- read_usage_lines,
296
- step,
297
- }: {
298
- action: UsageLineAction
299
- journal: Journal
300
- tracker: Tracker
301
- clock: EngineClock
302
- read_usage_lines: () => Promise<UsageLines>
303
- step: CommentStep
304
- }): Promise<void> => {
305
- const { spec_number, window, line, percent, until } = action
306
- if (action.type === 'start_usage_line_wait') {
307
- const { resets_at } = action
308
- await postCommentOnce({
309
- tracker,
310
- number: spec_number,
311
- body: usageLineWaitComment({ window, line, percent, resets_at }),
312
- step,
313
- n: 0,
314
- })
315
- journal.append({
316
- kind: 'usage_line_wait_started',
317
- ticket: null,
318
- role: null,
319
- content: { window, line, percent, resets_at, until },
320
- })
321
- return
322
- }
323
- const end = Date.parse(until)
324
- let reason: 'reset' | 'line_raised' = 'reset'
325
- for (;;) {
326
- const lines = await read_usage_lines()
327
- if (lines[LINE_OF[window]] > percent) {
328
- reason = 'line_raised'
329
- break
330
- }
331
- const left = end - clock.now()
332
- if (left <= 0) break
333
- await clock.sleep(Math.min(left, LIMIT_WAIT_NAP_MS))
334
- }
335
- await postCommentOnce({
336
- tracker,
337
- number: spec_number,
338
- body: usageLineEndedComment({ window, percent, reason }),
339
- step,
340
- n: 0,
341
- })
342
- journal.append({
343
- kind: 'usage_line_wait_ended',
344
- ticket: null,
345
- role: null,
346
- content: { until, reason },
347
- })
348
- }
349
-
350
- /**
351
- * Which try of its step an action is: the seq of the step's first try (its
352
- * first `step_started`), and whether this try redoes one a crash cut off.
353
- * A redo adopts the side effects its first try left behind: its comments,
354
- * its PR, its worktrees and commits.
355
- */
356
- export type StepTry = { first_seq: number; redo: boolean }
357
-
358
- /**
359
- * Carries out one action: talks to the tracker, then records what happened in
360
- * the journal. The only impure half of the engine; `decide` picks the action.
361
- *
362
- * Stop actions (`done`, `invalid_journal`) do nothing here.
363
- */
364
- export const executeAction = async ({
365
- action,
366
- journal,
367
- tracker,
368
- build,
369
- clock,
370
- reply_poll_ms,
371
- memory,
372
- step,
373
- read_usage_lines,
374
- }: {
375
- action: EngineAction
376
- journal: Journal
377
- tracker: Tracker
378
- /** Needed for every step after intake. */
379
- build?: BuildDeps
380
- /** Limit waits and reply waits sleep by it. Defaults to `SYSTEM_CLOCK`. */
381
- clock?: EngineClock
382
- /** How long a reply wait sleeps before it reads the spec issue. */
383
- reply_poll_ms?: number
384
- /** The memory client, for a run with memory on (#370). */
385
- memory?: MemoryDeps
386
- /**
387
- * Which try of its step this is, from the scheduler. Left out, it is a
388
- * first try, numbered after the journal's last record.
389
- */
390
- step?: StepTry
391
- /**
392
- * Reads the `luca-board` usage lines, again before each nap of a
393
- * usage-line wait. Left out, the lines never change during a wait.
394
- */
395
- read_usage_lines?: () => Promise<UsageLines>
396
- }): Promise<void> => {
397
- const tried: CommentStep = {
398
- run_id: basename(dirname(journal.file)),
399
- ...(step ?? {
400
- first_seq: (journal.read().at(-1)?.seq ?? 0) + 1,
401
- redo: false,
402
- }),
403
- }
404
- if (isMemoryAction(action)) {
405
- return executeMemoryAction({
406
- action,
407
- journal,
408
- tracker,
409
- memory,
410
- build,
411
- step: tried,
412
- })
413
- }
414
- switch (action.type) {
415
- case 'report_stuck':
416
- case 'wait_for_reply':
417
- case 'take_reply':
418
- case 'ignore_reply':
419
- case 'skip_ticket':
420
- case 'report_final_review_stuck':
421
- case 'ship_final_review':
422
- case 'retry_final_review':
423
- case 'mark_run_stuck':
424
- case 'report_run_stuck':
425
- return executeStuckAction({
426
- action,
427
- journal,
428
- tracker,
429
- clock: clock ?? SYSTEM_CLOCK,
430
- reply_poll_ms,
431
- step: tried,
432
- })
433
- case 'stop_for_crashes':
434
- journal.append({
435
- kind: 'run_stopped',
436
- ticket: null,
437
- role: null,
438
- content: {
439
- reason: action.reason,
440
- role: null,
441
- billing: false,
442
- crashed: true,
443
- },
444
- })
445
- return
446
- case 'record_usage':
447
- journal.append({
448
- kind: 'usage_recorded',
449
- ticket: action.usage.ticket,
450
- role: null,
451
- content: action.usage,
452
- })
453
- return
454
- case 'start_limit_wait':
455
- case 'wait_for_limit':
456
- case 'stop_for_billing':
457
- return executePlanAction({
458
- action,
459
- journal,
460
- tracker,
461
- clock: clock ?? SYSTEM_CLOCK,
462
- step: tried,
463
- })
464
- case 'start_usage_line_wait':
465
- case 'wait_for_usage_line':
466
- return executeUsageLineAction({
467
- action,
468
- journal,
469
- tracker,
470
- clock: clock ?? SYSTEM_CLOCK,
471
- read_usage_lines:
472
- read_usage_lines ??
473
- (async () => ({
474
- ...DEFAULT_USAGE_LINES,
475
- [LINE_OF[action.window]]: action.line,
476
- })),
477
- step: tried,
478
- })
479
- case 'read_intake':
480
- return readIntake({
481
- spec_number: action.spec_number,
482
- journal,
483
- tracker,
484
- })
485
- case 'refuse_intake':
486
- return refuseIntake({
487
- spec_number: action.spec_number,
488
- problems: action.problems,
489
- journal,
490
- tracker,
491
- step: tried,
492
- })
493
- case 'mark_already_done':
494
- case 'close_ticket':
495
- return executeAlreadyDoneAction({
496
- action,
497
- journal,
498
- tracker,
499
- step: tried,
500
- })
501
- case 'finish_nothing_to_do':
502
- journal.append({
503
- kind: 'nothing_to_do',
504
- ticket: null,
505
- role: null,
506
- content: {
507
- closed_tickets: action.closed_tickets,
508
- already_done: action.already_done,
509
- },
510
- })
511
- return
512
- case 'snapshot_intake': {
513
- const { spec, tickets, closed_tickets } = action.snapshot
514
- journal.append({
515
- kind: 'spec_snapshot',
516
- ticket: spec.number,
517
- role: null,
518
- content: {
519
- spec,
520
- ticket_order: tickets.map((ticket) => ticket.number),
521
- closed_tickets,
522
- },
523
- })
524
- for (const ticket of tickets) {
525
- journal.append({
526
- kind: 'ticket_snapshot',
527
- ticket: ticket.number,
528
- role: null,
529
- content: ticket,
530
- })
531
- }
532
- return
533
- }
534
- case 'invalid_journal':
535
- case 'done':
536
- return
537
- default:
538
- if (build === undefined) {
539
- throw new Error(
540
- `The engine needs a git adapter and an agent launcher to ${action.type}.`
541
- )
542
- }
543
- if (isFinalReviewAction(action)) {
544
- return executeFinalReviewAction({
545
- action,
546
- context: buildContext({
547
- journal,
548
- tracker,
549
- step: tried,
550
- ...build,
551
- }),
552
- })
553
- }
554
- if (action.type === 'write_changeset') {
555
- return writeChangeset({
556
- action,
557
- context: buildContext({
558
- journal,
559
- tracker,
560
- step: tried,
561
- ...build,
562
- }),
563
- })
564
- }
565
- return executeBuildAction({
566
- action,
567
- journal,
568
- tracker,
569
- step: tried,
570
- ...build,
571
- })
572
- }
573
- }
574
-
575
- /**
576
- * The ticket an action works on, or `null` for a run-level action. The
577
- * plan's actions are the whole run's, whichever ticket hit the limit; a
578
- * ticket's usage record is that ticket's. Reading and taking replies is
579
- * the run's too (see `keyOf`).
580
- */
581
- const ticketOf = (action: EngineAction): number | null => {
582
- switch (action.type) {
583
- case 'start_limit_wait':
584
- case 'wait_for_limit':
585
- case 'stop_for_billing':
586
- case 'wait_for_reply':
587
- case 'take_reply':
588
- case 'ignore_reply':
589
- return null
590
- case 'record_usage':
591
- return action.usage.ticket
592
- default:
593
- return 'ticket' in action && typeof action.ticket === 'number'
594
- ? action.ticket
595
- : null
596
- }
597
- }
598
-
599
- /**
600
- * Replies are read and taken beside the other steps, one at a time, under
601
- * their own key: waiting for a reply never holds a ticket up.
602
- */
603
- const REPLY_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
604
- 'wait_for_reply',
605
- 'take_reply',
606
- 'ignore_reply',
607
- ])
608
-
609
- /**
610
- * The scheduler's key for an action: at most one action per key runs at a
611
- * time. Reading and taking replies has its own (`replies`). Each lens has
612
- * its own (`lens:<lens>`), so the five lenses review at once; the final review's other steps share `final`; a ticket's steps its
613
- * number; and the run's own steps `run`, which only ever run alone.
614
- */
615
- export const keyOf = (action: EngineAction): string => {
616
- if (REPLY_ACTIONS.has(action.type)) return 'replies'
617
- // A search waits under the key of the step it comes before: its
618
- // ticket's, the final review's, or (the run's start) the run's.
619
- if (action.type === 'recall_memories') {
620
- if (action.ticket !== null) return String(action.ticket)
621
- return action.point === 'run_start' ? 'run' : 'final'
622
- }
623
- if (action.type === 'launch_lens') return `lens:${action.lens}`
624
- if (isFinalReviewAction(action)) return 'final'
625
- const ticket = ticketOf(action)
626
- return ticket === null ? 'run' : String(ticket)
627
- }
628
-
629
- /** Actions that are an agent's turn: their step names the role. */
630
- const AGENT_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
631
- 'launch_agent',
632
- 'follow_up_agent',
633
- 'launch_lens',
634
- 'launch_final_fixer',
635
- 'follow_up_final_fixer',
636
- ])
637
-
638
- /** The agent role of an agent's turn (the learner's is `learner`), else `null`. */
639
- const agentRoleOf = (action: EngineAction): string | null => {
640
- if (action.type === 'launch_learner') return 'learner'
641
- return AGENT_ACTIONS.has(action.type) &&
642
- 'role' in action &&
643
- action.role !== null
644
- ? action.role
645
- : null
646
- }
647
-
648
- /**
649
- * A step's name in the journal: its action type, plus `:<role>` for an
650
- * agent's turn, such as `follow_up_agent:implementer`.
651
- *
652
- * @example
653
- * stepOf({ type: 'run_gates', ticket: 11, target: 'ticket' }) // 'run_gates'
654
- */
655
- export const stepOf = (action: EngineAction): string => {
656
- const role = agentRoleOf(action)
657
- return role === null ? action.type : `${action.type}:${role}`
658
- }
659
-
660
- /**
661
- * Waits get no step records: a crash during one is no step's fault, and a
662
- * restarted engine just waits again (a limit wait until the same time).
663
- */
664
- const WAIT_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
665
- 'wait_for_reply',
666
- 'wait_for_limit',
667
- 'wait_for_usage_line',
668
- ])
669
-
670
- /**
671
- * Steps that can run for minutes: installs, test runs, and agents' turns.
672
- * The board hears of them as they start, so a run never looks frozen.
673
- */
674
- const SLOW_ACTIONS: ReadonlySet<EngineAction['type']> = new Set([
675
- ...AGENT_ACTIONS,
676
- 'launch_learner',
677
- 'install_dependencies',
678
- 'run_baseline_tests',
679
- 'run_red_check',
680
- 'run_gates',
681
- 'run_final_gates',
682
- ])
683
-
684
- /**
685
- * Actions that read or move the run branch. At most one runs at a time, so
686
- * joins, their gates, pushes, and new worktrees see one run branch. The
687
- * final review's fixers, gates, commit, and push work in the run branch's
688
- * worktree; its lenses only read it.
689
- */
690
- const usesRunBranch = (action: EngineAction): boolean => {
691
- switch (action.type) {
692
- case 'create_ticket_worktree':
693
- case 'join_run_branch':
694
- case 'push_run_branch':
695
- case 'rebase_ticket':
696
- case 'start_final_review':
697
- case 'launch_final_fixer':
698
- case 'follow_up_final_fixer':
699
- case 'run_final_gates':
700
- case 'commit_final_fix':
701
- case 'push_final_fixes':
702
- case 'write_changeset':
703
- case 'undo_join':
704
- case 'retry_ticket':
705
- return true
706
- case 'run_gates':
707
- case 'install_dependencies':
708
- return action.target === 'run_branch'
709
- // It puts back a run branch a join crashes cut off too often left.
710
- case 'mark_stuck':
711
- return action.reason === 'crashed'
712
- default:
713
- return false
714
- }
715
- }
716
-
717
- /**
718
- * Carries out one action with Jev in shadow mode: asks Jev before it (ticket
719
- * order, model, skills), then after it about the records it appended
720
- * (failure kinds, finding severities). Only the action's own ticket's new
721
- * records count, since other tickets append at the same time. Jev's answers
722
- * change nothing.
723
- */
724
- const executeWithJev = async ({
725
- jev,
726
- action,
727
- journal,
728
- tracker,
729
- build,
730
- clock,
731
- reply_poll_ms,
732
- memory,
733
- step,
734
- read_usage_lines,
735
- }: {
736
- jev: JevShadow
737
- action: EngineAction
738
- journal: Journal
739
- tracker: Tracker
740
- build?: BuildDeps
741
- clock?: EngineClock
742
- reply_poll_ms?: number
743
- memory?: MemoryDeps
744
- step?: StepTry
745
- read_usage_lines?: () => Promise<UsageLines>
746
- }): Promise<void> => {
747
- const shadow = { jev: jev.client, journal, timeout_ms: jev.timeout_ms }
748
- await askJevInShadow({
749
- ...shadow,
750
- asks: jevAsksBefore({
751
- action,
752
- state: replayRun({ records: journal.read() }),
753
- }),
754
- })
755
- const lastSeq = journal.read().at(-1)?.seq ?? 0
756
- await executeAction({
757
- action,
758
- journal,
759
- tracker,
760
- build,
761
- clock,
762
- reply_poll_ms,
763
- memory,
764
- step,
765
- read_usage_lines,
766
- })
767
- const records = journal.read()
768
- await askJevInShadow({
769
- ...shadow,
770
- asks: jevAsksAfter({
771
- records: records.filter(
772
- (record) =>
773
- record.seq > lastSeq && ownsRecord({ action, record })
774
- ),
775
- state: replayRun({ records }),
776
- }),
777
- })
778
- }
779
-
780
- /**
781
- * Whether a record appended while `action` ran is that action's own, since
782
- * other tickets (or other lenses) append at the same time: a ticket's
783
- * records for a ticket's step, a lens's for its lens, and the final
784
- * review's other ticket-less records for its other steps.
785
- */
786
- const ownsRecord = ({
787
- action,
788
- record,
789
- }: {
790
- action: EngineAction
791
- record: JournalRecord
792
- }): boolean => {
793
- if (action.type === 'launch_lens') {
794
- return (
795
- record.ticket === null &&
796
- (record.role === null || record.role === action.role)
797
- )
798
- }
799
- if (isFinalReviewAction(action)) {
800
- return record.ticket === null && !record.role?.endsWith('-lens')
801
- }
802
- const ticket = ticketOf(action)
803
- return ticket === null || record.ticket === ticket
804
- }
805
-
806
- /** The crash counts per key, from the journal's records. */
807
- const crashesIn = (records: JournalRecord[]): CrashCounts =>
808
- records.reduce<CrashCounts>(
809
- (crashes, record) => crashesAfter({ crashes, record }),
810
- {}
811
- )
812
-
813
- /**
814
- * The actions on keys a crash cut a step off first, in their order, then
815
- * the rest: a step a crash cut off (or the stop for it) starts before any
816
- * other, so a half-done change to the run branch, such as a join, is put
817
- * right before another step reads the run branch.
818
- */
819
- const redosFirst = ({
820
- actions,
821
- crashes,
822
- }: {
823
- actions: EngineAction[]
824
- crashes: CrashCounts
825
- }): EngineAction[] => {
826
- const [redos, rest] = partition(actions, (action) =>
827
- has(crashes, keyOf(action))
828
- )
829
- return [...redos, ...rest]
830
- }
831
-
832
60
  /** One action the engine started and has not seen settle yet. */
833
61
  type InFlight = { action: EngineAction; done: Promise<void> }
834
62
 
835
- /** The session a follow-up goes to, if the action is one. */
836
- const followUpSessionOf = (action: EngineAction): string[] =>
837
- action.type === 'follow_up_agent' || action.type === 'follow_up_final_fixer'
838
- ? [action.session_id]
839
- : []
840
-
841
63
  /**
842
64
  * Runs the engine: a scheduler over the decision step. Each pass it decides
843
65
  * every action that can run now from the journal and starts those it can,
@@ -862,8 +84,10 @@ const followUpSessionOf = (action: EngineAction): string[] =>
862
84
  * let finish, then the first error is thrown. `max_steps` counts the actions
863
85
  * started.
864
86
  *
865
- * With `jev`, Jev is asked around each step in **shadow mode** and its
866
- * answers are journaled but never acted on. Without it, nothing changes.
87
+ * With `jev`, the decision model is asked around each step in **shadow
88
+ * mode** and its answers are journaled but never acted on (see
89
+ * `shadowAsker`; a rejected token turns it off for the rest of this call).
90
+ * Without it, nothing changes.
867
91
  *
868
92
  * With `memory`, a run whose `run_started` turned memory on searches
869
93
  * MuninnDB at each recall point and saves the learner's memories at its
@@ -873,6 +97,11 @@ const followUpSessionOf = (action: EngineAction): string[] =>
873
97
  * With `board`, the whole journal is sent to the board once before the
874
98
  * first step and again after each step settles.
875
99
  *
100
+ * A ticket held on purpose between steps (another ticket's shared
101
+ * baseline, the join queue, the start queue) gets one `ticket_waiting` as
102
+ * its wait starts, from the same decision as the steps (`decideNext`),
103
+ * sent to the board at once.
104
+ *
876
105
  * A rejected plan limit is a limit wait: the engine sleeps by `clock` until
877
106
  * the window resets, then carries on. Overage or a billing error ends the
878
107
  * run for good (`done`, outcome `stopped`).
@@ -881,6 +110,16 @@ const followUpSessionOf = (action: EngineAction): string[] =>
881
110
  * decision it shares its readings with every run, reads the newest shared
882
111
  * ones and the lines, and pauses at a line (see `decide-usage-line.ts`).
883
112
  *
113
+ * With `going_runs`, a run started while an earlier run on the same repo
114
+ * is going waits for it before intake: a **repo wait** (see
115
+ * `decide-repo-wait.ts`).
116
+ *
117
+ * With `live_file`, the engine keeps the run's live file there while it
118
+ * runs (`live-file.ts`, #628): what each agent in a turn is doing now,
119
+ * never journaled, and each ticket check's wait for a prepare slot or a
120
+ * test slot while it lasts (#632). It is written a last time, with no
121
+ * agents, at the end.
122
+ *
884
123
  * Crash recovery: every step it starts (not a wait) is journaled between a
885
124
  * `step_started` and a `step_ended` (none when the step throws). On start,
886
125
  * a step with no `step_ended` was cut off by a crash: one `run_resumed`
@@ -909,6 +148,8 @@ export const runEngine = async ({
909
148
  reply_poll_ms,
910
149
  memory,
911
150
  usage,
151
+ going_runs,
152
+ live_file,
912
153
  }: Partial<BuildDeps> & {
913
154
  journal: Journal
914
155
  tracker: Tracker
@@ -916,7 +157,7 @@ export const runEngine = async ({
916
157
  max_steps?: number
917
158
  /** Action types to stop at without carrying them out, such as in tests. */
918
159
  stop_before?: EngineAction['type'][]
919
- /** Jev in shadow mode. Leave it out to run without Jev. */
160
+ /** The decision model in shadow mode. Leave it out to run without one. */
920
161
  jev?: JevShadow
921
162
  /** Sends the journal to the board after every step. Never throws. */
922
163
  board?: BoardSync
@@ -931,18 +172,32 @@ export const runEngine = async ({
931
172
  memory?: MemoryDeps
932
173
  /** The usage lines and the shared readings. Left out, there is no usage line. */
933
174
  usage?: UsageLineDeps
175
+ /**
176
+ * Lists the unfinished runs (`goingRuns`), for the repo wait before
177
+ * intake. Left out, there is no repo wait.
178
+ */
179
+ going_runs?: GoingRunsReader
180
+ /**
181
+ * Where to keep the run's live file (#628), such as
182
+ * `runLiveFilePath({ runs_dir, run_id })`. Left out, there is none.
183
+ */
184
+ live_file?: string
934
185
  }): Promise<EngineAction> => {
935
186
  const limit = max_steps ?? DEFAULT_MAX_STEPS
936
187
  const stops = new Set([...STOP_ACTIONS, ...(stop_before ?? [])])
188
+ const live =
189
+ live_file === undefined ? null : createLiveFile({ file: live_file })
937
190
  const build =
938
191
  git === undefined || launcher === undefined
939
192
  ? undefined
940
- : { git, launcher }
193
+ : { git, launcher: live?.launcher(launcher) ?? launcher }
941
194
  const inFlight = new Map<string, InFlight>()
942
195
  let started = 0
196
+ // The run's own asker, so turning the decision model off lasts this run.
197
+ const ask = jev === undefined ? undefined : shadowAsker({ jev, journal })
943
198
 
944
199
  const execute = (action: EngineAction, step?: StepTry): Promise<void> =>
945
- jev === undefined
200
+ ask === undefined
946
201
  ? executeAction({
947
202
  action,
948
203
  journal,
@@ -953,9 +208,10 @@ export const runEngine = async ({
953
208
  memory,
954
209
  step,
955
210
  read_usage_lines: usage?.read_lines,
211
+ going_runs,
956
212
  })
957
213
  : executeWithJev({
958
- jev,
214
+ ask,
959
215
  action,
960
216
  journal,
961
217
  tracker,
@@ -965,18 +221,27 @@ export const runEngine = async ({
965
221
  memory,
966
222
  step,
967
223
  read_usage_lines: usage?.read_lines,
224
+ going_runs,
968
225
  })
969
226
 
970
- /** The decision step, with the usage line when the run has one. */
227
+ /**
228
+ * The decision step, with the usage line when the run has one, and the
229
+ * earlier runs on the repo while the run has yet to look for them.
230
+ */
971
231
  const decideNow = async (
972
232
  records: JournalRecord[]
973
- ): Promise<EngineAction[]> => {
974
- if (usage === undefined) return decideSteps({ records })
233
+ ): Promise<ReturnType<typeof decideNext>> => {
234
+ const earlier_runs =
235
+ going_runs !== undefined && repoWaitUnchecked({ records })
236
+ ? (await readEarlierRuns({ going_runs, journal })).runs
237
+ : undefined
238
+ if (usage === undefined) return decideNext({ records, earlier_runs })
975
239
  await usage.share(runReadings({ records }))
976
- return decideSteps({
240
+ return decideNext({
977
241
  records,
978
242
  usage_lines: await usage.read_lines(),
979
243
  shared_readings: await usage.read_shared(),
244
+ earlier_runs,
980
245
  })
981
246
  }
982
247
  const settleAll = () =>
@@ -1014,10 +279,18 @@ export const runEngine = async ({
1014
279
  ...who,
1015
280
  content: { key, step, first_seq },
1016
281
  })
1017
- await execute(action, {
1018
- first_seq: first_seq ?? started.seq,
1019
- redo: first_seq !== null,
1020
- })
282
+ const run = () =>
283
+ execute(action, {
284
+ first_seq: first_seq ?? started.seq,
285
+ redo: first_seq !== null,
286
+ })
287
+ // A ticket's check waiting for a slot shows in the live file (#632).
288
+ await (live === null || who.ticket === null
289
+ ? run()
290
+ : hearSlotWaits({
291
+ listener: live.slotWaits({ ticket: who.ticket }),
292
+ run,
293
+ }))
1021
294
  journal.append({ kind: 'step_ended', ...who, content: { key, step } })
1022
295
  }
1023
296
 
@@ -1048,10 +321,8 @@ export const runEngine = async ({
1048
321
  await closeFinished({ all: false })
1049
322
  const records = journal.read()
1050
323
  const crashes = crashesIn(records)
1051
- const actions = redosFirst({
1052
- actions: await decideNow(records),
1053
- crashes,
1054
- })
324
+ const decided = await decideNow(records)
325
+ const actions = redosFirst({ actions: decided.actions, crashes })
1055
326
  const stop = actions.find((action) => stops.has(action.type))
1056
327
  if (stop !== undefined && inFlight.size === 0) {
1057
328
  if (STOP_ACTIONS.has(stop.type)) {
@@ -1060,7 +331,24 @@ export const runEngine = async ({
1060
331
  return stop
1061
332
  }
1062
333
  let slowStarted = false
334
+ let waited = false
1063
335
  if (stop === undefined) {
336
+ // A wait between steps is journaled once, as it starts, and
337
+ // shows on the board at once: the step it waits on may run
338
+ // for minutes.
339
+ for (const wait of decided.waits) {
340
+ journal.append({
341
+ kind: 'ticket_waiting',
342
+ ticket: wait.ticket,
343
+ role: null,
344
+ content: {
345
+ for: wait.for,
346
+ on_ticket: wait.on_ticket,
347
+ since: new Date().toISOString(),
348
+ },
349
+ })
350
+ waited = true
351
+ }
1064
352
  for (const action of actions) {
1065
353
  const key = keyOf(action)
1066
354
  if (!canStart({ action, key })) continue
@@ -1085,8 +373,11 @@ export const runEngine = async ({
1085
373
  if (SLOW_ACTIONS.has(action.type)) slowStarted = true
1086
374
  }
1087
375
  }
1088
- // A slow step shows on the board while it runs, not only once it ends.
1089
- if (slowStarted) await board?.sync({ records: journal.read() })
376
+ // A slow step shows on the board while it runs, not only once it
377
+ // ends; so does a wait.
378
+ if (slowStarted || waited) {
379
+ await board?.sync({ records: journal.read() })
380
+ }
1090
381
  if (inFlight.size === 0) {
1091
382
  throw new Error(
1092
383
  `The engine could start none of: ${actions.map(({ type }) => type).join(', ')}.`
@@ -1120,5 +411,7 @@ export const runEngine = async ({
1120
411
  // The run ends here, so no session stays open.
1121
412
  await closeFinished({ all: true })
1122
413
  throw error
414
+ } finally {
415
+ live?.close()
1123
416
  }
1124
417
  }