@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,80 +1,53 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { mkdir } from 'node:fs/promises'
3
- import { basename, dirname, join } from 'node:path'
4
-
5
- import difference from 'lodash/difference'
6
- import uniq from 'lodash/uniq'
2
+ import { basename, join } from 'node:path'
7
3
 
4
+ import { followUpAgent, launchAgent } from './agent-turn'
8
5
  import type { AlreadyDoneAction } from './already-done'
6
+ import {
7
+ buildContext,
8
+ need,
9
+ reportFile,
10
+ testFilesIn,
11
+ ticketWorktree,
12
+ type BuildDeps,
13
+ } from './build-context'
9
14
  import { CHANGESET_CONFIG } from './changeset'
10
- import { mayEditTests, type BuildAction } from './decide-build'
15
+ import { commitTicket } from './commit-in'
16
+ import type { BuildAction } from './decide-build'
11
17
  import type { FinalReviewAction } from './decide-final-review'
18
+ import { checkAlreadyDoneIn } from './execute-already-done-check'
19
+ import { gatesIn, installIn } from './execute-gates'
20
+ import {
21
+ rebaseTicket,
22
+ sendTestsBack,
23
+ undoCutOffJoin,
24
+ undoJoin,
25
+ } from './execute-join'
26
+ import { runRedCheck } from './execute-red-check'
12
27
  import { retryTicket } from './execute-stuck'
13
- import { openTicketsNotInRun } from './not-in-run'
14
- import { withNotInRun } from './pull-request-text'
15
28
  import { closeSessions, openSessionsIn } from './session-close'
29
+ import { watchPrepare } from './worktree-changes'
16
30
 
17
- import type { AgentLauncher, AgentTurn } from '../agents/agent-launcher'
18
- import {
19
- parseRoleResult,
20
- type AgentRole,
21
- type RoleResult,
22
- } from '../agents/role-results'
23
- import { bunTestCommands, type EngineConfig } from '../config/engine-config'
24
- import { prepareCheck, runGates, shellCheck } from '../gates/gate-runner'
25
- import { endedText } from '../gates/gate-schemas'
26
- import { newCodeFiles, importStem, scanLeftovers } from '../gates/leftover-scan'
27
- import {
28
- dependenciesChanged,
29
- installCommand,
30
- MANIFEST,
31
- newWorktreeInstall,
32
- rebaseNeedsInstall,
33
- } from '../gates/lockfile-install'
34
- import { checkRed } from '../gates/red-check'
35
- import { runBunTests, testFilesAmong } from '../gates/test-runner'
36
- import type { FileChange, GitAdapter } from '../git/git-adapter'
37
- import { pathText } from '../git/path-text'
38
- import { describeViolations } from '../guards/after-turn-check'
39
- import { guardRoleOf } from '../guards/role-rules'
40
- import { enforceAfterTurn, snapshotWorktree } from '../guards/worktree-state'
31
+ import { bunTestCommands, testTimeoutOf } from '../config/engine-config'
32
+ import { prepareCheck } from '../gates/gate-runner'
33
+ import { runBunTests } from '../gates/test-runner'
41
34
  import type { Journal } from '../journal/journal'
42
- import type {
43
- AgentFailure,
44
- CommitStage,
45
- GateTarget,
46
- JournalRecord,
47
- } from '../journal/journal-record'
48
- import {
49
- replayRun,
50
- type ReplayedWorktree,
51
- type RunState,
52
- } from '../journal/replay'
53
- import { sessionSignal } from '../limits/plan-signals'
54
- import { createAgentMessaging } from '../messages/agent-messaging'
55
35
  import type { CommentStep } from '../tracker/post-comment-once'
56
36
  import type { Tracker } from '../tracker/tracker'
57
37
 
58
- /** What the engine needs, beyond the journal and tracker, to build tickets. */
59
- export type BuildDeps = {
60
- git: GitAdapter
61
- launcher: AgentLauncher
62
- }
63
-
64
- /** What a build step's executor works with. */
65
- export type BuildContext = BuildDeps & {
66
- journal: Journal
67
- tracker: Tracker
68
- state: RunState
69
- config: EngineConfig
70
- /** The run's folder, next to its journal and outside git. */
71
- run_dir: string
72
- /**
73
- * Which try of its step this is: a redo after a crash adopts what its
74
- * first try left behind.
75
- */
76
- step: CommentStep
77
- }
38
+ export { runTurn } from './agent-turn'
39
+ export {
40
+ buildContext,
41
+ need,
42
+ ticketWorktree,
43
+ type BuildContext,
44
+ type BuildDeps,
45
+ } from './build-context'
46
+ export { commitIn, dropAgentChangesets } from './commit-in'
47
+ export { gatesIn, slowTestsIn } from './execute-gates'
48
+ export { openJoin } from './execute-join'
49
+ export { NO_RED_CHANGES } from './execute-red-check'
50
+ export { leaveOutIn } from './worktree-changes'
78
51
 
79
52
  /** The run branch's name: one per run, so runs never share a branch. */
80
53
  export const runBranchName = ({
@@ -85,842 +58,6 @@ export const runBranchName = ({
85
58
  run_id: string
86
59
  }): string => `luca/spec-${spec_number}-${run_id}`
87
60
 
88
- /** A journaled value, or a clear error that the journal has none yet. */
89
- export const need = <T>({
90
- value,
91
- what,
92
- }: {
93
- value: T | null | undefined
94
- what: string
95
- }): T => {
96
- if (value === null || value === undefined) {
97
- throw new Error(`The journal has no ${what} yet.`)
98
- }
99
- return value
100
- }
101
-
102
- export const ticketWorktree = ({
103
- state,
104
- ticket,
105
- }: {
106
- state: RunState
107
- ticket: number
108
- }): ReplayedWorktree =>
109
- need({
110
- value: state.tickets[ticket]?.worktree,
111
- what: `worktree for #${ticket}`,
112
- })
113
-
114
- /** A fresh report file outside the worktree, so it is never a leftover. */
115
- const reportFile = async ({
116
- context,
117
- ticket,
118
- label,
119
- }: {
120
- context: BuildContext
121
- /** `null` for the final review. */
122
- ticket: number | null
123
- label: string
124
- }): Promise<string> => {
125
- const folder = join(context.run_dir, 'reports')
126
- await mkdir(folder, { recursive: true })
127
- return join(
128
- folder,
129
- `${context.state.last_seq + 1}-${ticket ?? 'final'}-${label}.xml`
130
- )
131
- }
132
-
133
- const testFilesIn = async ({
134
- context,
135
- cwd,
136
- }: {
137
- context: BuildContext
138
- cwd: string
139
- }): Promise<string[]> =>
140
- testFilesAmong({
141
- files: await context.git.listFiles({ cwd }),
142
- test_file_patterns: context.config.test_file_patterns,
143
- })
144
-
145
- /**
146
- * Runs `run`, a run of the config's prepare command in `cwd`, and journals
147
- * as `prepare_made` the untracked paths it made: there after it ran, not
148
- * before, and not known yet. Without a prepare command it only runs `run`.
149
- *
150
- * A crash in the middle of a prepare run leaves what it made so far; the
151
- * redo's own "before" then holds those paths, so they count as the
152
- * ticket's unless an earlier prepare run already noted them.
153
- */
154
- const watchPrepare = async <Result>({
155
- context,
156
- cwd,
157
- ticket,
158
- run,
159
- }: {
160
- context: BuildContext
161
- cwd: string
162
- /** The step's ticket, `null` in the final review. */
163
- ticket: number | null
164
- run: () => Promise<Result>
165
- }): Promise<Result> => {
166
- if (context.config.prepare === undefined) return run()
167
- const before = await context.git.untracked({ cwd })
168
- const result = await run()
169
- const paths = difference(
170
- await context.git.untracked({ cwd }),
171
- before,
172
- context.state.prepare_made
173
- )
174
- if (paths.length > 0) {
175
- context.journal.append({
176
- kind: 'prepare_made',
177
- ticket,
178
- role: null,
179
- content: { paths },
180
- })
181
- }
182
- return result
183
- }
184
-
185
- /** A ticket's changes, less what the prepare command made. */
186
- const ticketChanges = async ({
187
- context,
188
- cwd,
189
- }: {
190
- context: BuildContext
191
- cwd: string
192
- }): Promise<FileChange[]> =>
193
- (await context.git.changes({ cwd })).filter(
194
- ({ path }) => !context.state.prepare_made.includes(path)
195
- )
196
-
197
- const runRedCheck = async ({
198
- context,
199
- action,
200
- }: {
201
- context: BuildContext
202
- action: Extract<BuildAction, { type: 'run_red_check' }>
203
- }) => {
204
- const { path } = ticketWorktree({
205
- state: context.state,
206
- ticket: action.ticket,
207
- })
208
- const baseline = need({
209
- value: context.state.tickets[action.ticket]?.baseline,
210
- what: `baseline test run for #${action.ticket}`,
211
- })
212
- const test_files = await testFilesIn({ context, cwd: path })
213
- const prepare = await watchPrepare({
214
- context,
215
- cwd: path,
216
- ticket: action.ticket,
217
- run: () => prepareCheck({ config: context.config, cwd: path }),
218
- })
219
- const current = await runBunTests({
220
- cwd: path,
221
- commands: bunTestCommands(context),
222
- test_files,
223
- report_file: await reportFile({
224
- context,
225
- ticket: action.ticket,
226
- label: 'red',
227
- }),
228
- })
229
- const files = uniq(
230
- action.mapping.flatMap(({ tests }) => tests.map(({ file }) => file))
231
- )
232
- const sources: Record<string, string | null> = {}
233
- for (const file of files) {
234
- sources[file] = await pathText({ path: join(path, file) })
235
- }
236
- const checked = checkRed({
237
- criteria_ids: action.criteria_ids,
238
- mapping: action.mapping,
239
- baseline,
240
- current,
241
- test_files,
242
- sources,
243
- })
244
- // A failed prepare after the test-writer's turn is one more problem,
245
- // for the test-writer's fix loop.
246
- const result =
247
- prepare === null || prepare.ok
248
- ? checked
249
- : {
250
- ...checked,
251
- ok: false,
252
- problems: [
253
- `The prepare command \`${prepare.command}\` failed (${endedText(prepare)}):\n${prepare.output}`,
254
- ...checked.problems,
255
- ],
256
- }
257
- context.journal.append({
258
- kind: 'red_check',
259
- ticket: action.ticket,
260
- role: null,
261
- content: { ...result, tests: current },
262
- })
263
- }
264
-
265
- /** Whether anything besides the file itself mentions a new code file. */
266
- const isUsed = async ({
267
- context,
268
- cwd,
269
- path,
270
- added_texts,
271
- }: {
272
- context: BuildContext
273
- cwd: string
274
- path: string
275
- added_texts: Record<string, string>
276
- }): Promise<boolean> => {
277
- const stem = importStem(path)
278
- const tracked = await context.git.filesMentioning({ cwd, text: stem })
279
- if (tracked.some((file) => file !== path)) return true
280
- return Object.entries(added_texts).some(
281
- ([file, text]) => file !== path && text.includes(stem)
282
- )
283
- }
284
-
285
- /**
286
- * The commit a crash left behind at `cwd`: HEAD carries `message` and no
287
- * record names it yet, so the step's first try made it and died before
288
- * journaling it. `null` when HEAD is no such commit.
289
- */
290
- const leftCommit = async ({
291
- context,
292
- cwd,
293
- message,
294
- }: {
295
- context: BuildContext
296
- cwd: string
297
- message: string
298
- }): Promise<{ sha: string; message: string; files: string[] } | null> => {
299
- const last = await context.git.lastCommit({ cwd })
300
- if (last.message !== message.trim()) return null
301
- const journaled = context.journal
302
- .read()
303
- .some(
304
- (record) =>
305
- (record.kind === 'commit_made' &&
306
- record.content.sha === last.sha) ||
307
- (record.kind === 'ticket_joined' &&
308
- record.content.ok &&
309
- record.content.shas.includes(last.sha))
310
- )
311
- return journaled ? null : { sha: last.sha, message, files: last.files }
312
- }
313
-
314
- /**
315
- * The leftover scan, then an engine commit of everything in `cwd`, both
316
- * journaled. What the prepare command made (`prepare_made`) is neither
317
- * scanned nor committed. A hit blocks the commit. A `fix` commit with
318
- * nothing to commit (every finding was a "won't fix") journals the current
319
- * commit with no files instead. A redo after a crash that already committed
320
- * (nothing left to commit, and HEAD is an unjournaled commit with this
321
- * message) journals that commit instead of making another; its scan, of a
322
- * clean worktree, finds nothing, as the first try's did before it committed.
323
- */
324
- export const commitIn = async ({
325
- context,
326
- cwd,
327
- ticket,
328
- stage,
329
- message,
330
- mention_text,
331
- }: {
332
- context: BuildContext
333
- cwd: string
334
- /** `null` for the final review. */
335
- ticket: number | null
336
- stage: CommitStage
337
- message: string
338
- /** The spec and ticket text a new markdown file may be named in. */
339
- mention_text: string
340
- }) => {
341
- const changes = await ticketChanges({ context, cwd })
342
- const test_files = testFilesAmong({
343
- files: changes.map(({ path }) => path),
344
- test_file_patterns: context.config.test_file_patterns,
345
- })
346
- const added = changes.filter(({ change }) => change === 'added')
347
- const added_texts: Record<string, string> = {}
348
- for (const { path } of added) {
349
- added_texts[path] = (await pathText({ path: join(cwd, path) })) ?? ''
350
- }
351
- const used_code: Record<string, boolean> = {}
352
- for (const path of newCodeFiles({ changes, test_files })) {
353
- used_code[path] = await isUsed({ context, cwd, path, added_texts })
354
- }
355
- const hits = scanLeftovers({ changes, test_files, mention_text, used_code })
356
- context.journal.append({
357
- kind: 'leftover_scan',
358
- ticket,
359
- role: null,
360
- content: { stage, hits },
361
- })
362
- if (hits.length > 0) return
363
- const adopted =
364
- changes.length === 0 && context.step.redo
365
- ? await leftCommit({ context, cwd, message })
366
- : null
367
- if (adopted !== null) {
368
- context.journal.append({
369
- kind: 'commit_made',
370
- ticket,
371
- role: null,
372
- content: { stage, ...adopted },
373
- })
374
- return
375
- }
376
- if (changes.length === 0 && stage === 'fix') {
377
- // The fixers changed nothing (every finding was a "won't fix"): no
378
- // commit to make, so the re-review's new changes are empty.
379
- context.journal.append({
380
- kind: 'commit_made',
381
- ticket,
382
- role: null,
383
- content: {
384
- stage,
385
- sha: await context.git.head({ cwd }),
386
- message,
387
- files: [],
388
- },
389
- })
390
- return
391
- }
392
- const commit = await context.git.commitAll({
393
- cwd,
394
- message,
395
- leave_out: context.state.prepare_made,
396
- })
397
- context.journal.append({
398
- kind: 'commit_made',
399
- ticket,
400
- role: null,
401
- content: { stage, sha: commit.sha, message, files: commit.files },
402
- })
403
- }
404
-
405
- const commitTicket = async ({
406
- context,
407
- action,
408
- }: {
409
- context: BuildContext
410
- action: Extract<BuildAction, { type: 'commit_ticket' }>
411
- }) => {
412
- const { path: cwd } = ticketWorktree({
413
- state: context.state,
414
- ticket: action.ticket,
415
- })
416
- const snapshot = need({
417
- value: context.state.snapshot,
418
- what: 'spec snapshot',
419
- })
420
- const ticket = snapshot.tickets[action.ticket]
421
- await commitIn({
422
- context,
423
- cwd,
424
- ticket: action.ticket,
425
- stage: action.stage,
426
- message: action.message,
427
- mention_text: [snapshot.spec.body, ticket?.body ?? ''].join('\n'),
428
- })
429
- }
430
-
431
- const failTurn = ({
432
- context,
433
- ticket,
434
- role,
435
- error,
436
- failure,
437
- session_id,
438
- }: {
439
- context: BuildContext
440
- ticket: number | null
441
- role: AgentRole
442
- error: string
443
- failure: AgentFailure
444
- session_id: string | null
445
- }) => {
446
- context.journal.append({
447
- kind: 'agent_failed',
448
- ticket,
449
- role,
450
- content: { role, error, failure, session_id },
451
- })
452
- }
453
-
454
- /**
455
- * Runs one agent turn, a launch or a follow-up, and journals how it ended.
456
- * Before the turn the engine snapshots the worktree; after it, the
457
- * after-turn check undoes and reports anything the role may not change, for
458
- * every launcher alike; what others changed in the shared `.git` meanwhile
459
- * is journaled as `shared_git_changed`. Then the result is judged by its
460
- * structured output. Each failed turn is journaled once as `agent_failed`
461
- * with how it failed; the decision step picks what happens next. A launcher stop journals
462
- * `run_stopped` and ends the run. A turn the plan cut off (a rejected limit,
463
- * overage, a billing error) journals only its session. Both close the
464
- * turn's session first, since no follow-up can reach it.
465
- *
466
- * @param worktree - Where the agent works: its ticket's worktree, or the run
467
- * branch's for the final review.
468
- * @param ticket - The ticket its records name, `null` for the final review.
469
- * @returns The role's result when the agent finished, else `null`.
470
- */
471
- export const runTurn = async ({
472
- context,
473
- worktree,
474
- ticket,
475
- role,
476
- may_edit_tests,
477
- start,
478
- }: {
479
- context: BuildContext
480
- worktree: ReplayedWorktree
481
- ticket: number | null
482
- role: AgentRole
483
- may_edit_tests: boolean
484
- start: (cwd: string) => Promise<AgentTurn>
485
- }): Promise<RoleResult | null> => {
486
- const { path, branch } = worktree
487
- const before = await snapshotWorktree({
488
- cwd: path,
489
- branch,
490
- config: context.config,
491
- })
492
- const turn = await start(path)
493
- if (turn.session !== undefined) {
494
- context.journal.append({
495
- kind: 'agent_session',
496
- ticket,
497
- role,
498
- content: { role, session: turn.session },
499
- })
500
- }
501
- const guard_role = guardRoleOf({ role })
502
- const { violations, outside } = await enforceAfterTurn({
503
- cwd: path,
504
- branch,
505
- role: guard_role,
506
- may_edit_tests,
507
- config: context.config,
508
- before,
509
- leave_out: context.state.prepare_made,
510
- })
511
- // Others' changes to the shared .git: noted, never blamed or undone.
512
- if (outside.length > 0) {
513
- context.journal.append({
514
- kind: 'shared_git_changed',
515
- ticket,
516
- role,
517
- content: { role, changes: outside },
518
- })
519
- }
520
- // The plan cut the turn off. Its session, journaled above, holds why,
521
- // and the decision step reads it from there: a limit wait or a billing
522
- // stop. The turn uses up no try, and its step is taken again. With no
523
- // sign in the session, the engine can't tell which, so it stops.
524
- const unexplained =
525
- !turn.ok &&
526
- turn.failure === 'plan' &&
527
- (turn.session === undefined ||
528
- sessionSignal({ session: turn.session }).kind === 'ok')
529
- // Either way no follow-up reaches the session: close it now.
530
- const cut = !turn.ok && (turn.failure === 'plan' || turn.failure === 'stop')
531
- if (cut && turn.session_id !== undefined) {
532
- await closeSessions({
533
- journal: context.journal,
534
- launcher: context.launcher,
535
- sessions: [{ session_id: turn.session_id, ticket, role }],
536
- })
537
- }
538
- if (!turn.ok && turn.failure === 'plan' && !unexplained) return null
539
- if (!turn.ok && (turn.failure === 'stop' || unexplained)) {
540
- context.journal.append({
541
- kind: 'run_stopped',
542
- ticket,
543
- role,
544
- content: { reason: turn.error, role, billing: false },
545
- })
546
- throw new Error(`Run stopped: ${turn.error}`)
547
- }
548
- const session_id = turn.session_id ?? null
549
- const fail = (failure: AgentFailure, error: string) => {
550
- failTurn({ context, ticket, role, error, failure, session_id })
551
- return null
552
- }
553
- if (violations.length > 0) {
554
- return fail(
555
- 'guard',
556
- describeViolations({ role: guard_role, violations })
557
- )
558
- }
559
- if (!turn.ok) {
560
- return fail(turn.failure === 'engine' ? 'engine' : 'agent', turn.error)
561
- }
562
- if (
563
- turn.structured_output === undefined ||
564
- turn.structured_output === null
565
- ) {
566
- return fail('result', `The ${role} finished with no structured output.`)
567
- }
568
- const checked = parseRoleResult({ role, output: turn.structured_output })
569
- if (!checked.ok) return fail('result', checked.error)
570
- context.journal.append({
571
- kind: 'agent_finished',
572
- ticket,
573
- role,
574
- content: { ...checked.value, session_id },
575
- })
576
- return checked.value
577
- }
578
-
579
- const launchAgent = async ({
580
- context,
581
- action,
582
- }: {
583
- context: BuildContext
584
- action: Extract<BuildAction, { type: 'launch_agent' }>
585
- }) => {
586
- const { ticket, role, prompt, may_edit_tests } = action
587
- context.journal.append({
588
- kind: 'agent_started',
589
- ticket,
590
- role,
591
- content: { role, prompt, follow_up_of: null },
592
- })
593
- await runTurn({
594
- context,
595
- worktree: ticketWorktree({ state: context.state, ticket }),
596
- ticket,
597
- role,
598
- may_edit_tests,
599
- start: (cwd) =>
600
- context.launcher.launch({
601
- role,
602
- ticket,
603
- prompt,
604
- cwd,
605
- may_edit_tests,
606
- config: context.config,
607
- messaging: createAgentMessaging({
608
- journal: context.journal,
609
- ticket,
610
- role,
611
- }),
612
- }),
613
- })
614
- }
615
-
616
- const followUpAgent = async ({
617
- context,
618
- action,
619
- }: {
620
- context: BuildContext
621
- action: Extract<BuildAction, { type: 'follow_up_agent' }>
622
- }) => {
623
- const { ticket, role, session_id, message } = action
624
- const snapshot = need({
625
- value: context.state.snapshot?.tickets[ticket],
626
- what: `snapshot of #${ticket}`,
627
- })
628
- context.journal.append({
629
- kind: 'agent_started',
630
- ticket,
631
- role,
632
- content: { role, prompt: message, follow_up_of: session_id },
633
- })
634
- await runTurn({
635
- context,
636
- worktree: ticketWorktree({ state: context.state, ticket }),
637
- ticket,
638
- role,
639
- may_edit_tests: mayEditTests({ role, ticket: snapshot }),
640
- start: (cwd) =>
641
- context.launcher.followUp({
642
- session_id,
643
- role,
644
- ticket,
645
- message,
646
- cwd,
647
- config: context.config,
648
- }),
649
- })
650
- }
651
-
652
- /**
653
- * Where the run branch stood before a join of `ticket` a crash cut off: the
654
- * latest `join_started` for it with no `ticket_joined` or `ticket_stuck`
655
- * for it after, else `null`.
656
- *
657
- * @example
658
- * openJoin({ records: journal.read(), ticket: 11 }) // 'a1b2c3...' after a crash mid-join
659
- */
660
- export const openJoin = ({
661
- records,
662
- ticket,
663
- }: {
664
- records: JournalRecord[]
665
- ticket: number
666
- }): string | null => {
667
- const last = records.findLast(
668
- (record) =>
669
- record.ticket === ticket &&
670
- (record.kind === 'join_started' ||
671
- record.kind === 'ticket_joined' ||
672
- record.kind === 'ticket_stuck')
673
- )
674
- return last?.kind === 'join_started' ? last.content.run_branch_sha : null
675
- }
676
-
677
- /**
678
- * Puts the run branch back where it stood before a join of `ticket` a crash
679
- * cut off, dropping whatever half of its commits the crash left there. A
680
- * cut-off step is taken again before other steps start, so nothing else
681
- * moved the run branch since. Does nothing when no join was cut off.
682
- */
683
- const undoCutOffJoin = async ({
684
- context,
685
- ticket,
686
- }: {
687
- context: BuildContext
688
- ticket: number
689
- }) => {
690
- const sha = openJoin({ records: context.journal.read(), ticket })
691
- if (sha === null) return
692
- const runBranch = need({
693
- value: context.state.run_branch,
694
- what: 'run branch',
695
- })
696
- await context.git.resetWorktree({ cwd: runBranch.path, to: sha })
697
- }
698
-
699
- /**
700
- * Undoes a ticket's join on the run branch, back to before `first_sha`, and
701
- * returns the commits undone. The undone join's install is still in the run
702
- * branch's `node_modules`, so when its commits changed dependency files the
703
- * lockfile's install runs there again.
704
- */
705
- const undoJoin = async ({
706
- context,
707
- ticket,
708
- first_sha,
709
- }: {
710
- context: BuildContext
711
- ticket: number
712
- first_sha: string
713
- }): Promise<string[]> => {
714
- const { git, state, journal } = context
715
- const runBranch = need({ value: state.run_branch, what: 'run branch' })
716
- const joined = state.tickets[ticket]?.joined
717
- const last = joined?.ok ? joined.shas.at(-1) : undefined
718
- const undoneFiles =
719
- last === undefined
720
- ? []
721
- : await git.filesBetween({
722
- cwd: runBranch.path,
723
- from: `${first_sha}^`,
724
- to: last,
725
- })
726
- const { undone } = await git.undoReplay({ cwd: runBranch.path, first_sha })
727
- // A redo whose first try undid the join before a crash finds nothing
728
- // left to undo: the commits it undid are the journaled join's.
729
- const named =
730
- undone.length === 0 && context.step.redo && joined?.ok === true
731
- ? joined.shas
732
- : undone
733
- if (dependenciesChanged({ changed_files: undoneFiles })) {
734
- await installIn({
735
- journal,
736
- cwd: runBranch.path,
737
- target: 'run_branch',
738
- ticket: null,
739
- })
740
- }
741
- return named
742
- }
743
-
744
- /**
745
- * Puts a joined ticket's change back on top of the run branch: undoes the
746
- * join first if asked (its gates failed), then resets the ticket's worktree
747
- * to the run branch's tip and applies the ticket's whole diff there,
748
- * uncommitted, with conflict markers in the files that clash. Journals the
749
- * clashed files split into tests and code.
750
- */
751
- const rebaseTicket = async ({
752
- context,
753
- action,
754
- }: {
755
- context: BuildContext
756
- action: Extract<BuildAction, { type: 'rebase_ticket' }>
757
- }) => {
758
- const { git, state, journal, config } = context
759
- const worktree = ticketWorktree({ state, ticket: action.ticket })
760
- const runBranch = need({ value: state.run_branch, what: 'run branch' })
761
- const progress = state.tickets[action.ticket]
762
- // The ticket's latest commit: a review fix round's, or the green one.
763
- const head = need({
764
- value: progress?.commits.fix ?? progress?.commits.green,
765
- what: `green commit for #${action.ticket}`,
766
- })
767
- const undone =
768
- action.undo_first_sha === null
769
- ? []
770
- : await undoJoin({
771
- context,
772
- ticket: action.ticket,
773
- first_sha: action.undo_first_sha,
774
- })
775
- const onto = await git.head({ cwd: runBranch.path })
776
- const moved_files = await git.filesBetween({
777
- cwd: worktree.path,
778
- from: worktree.base_sha,
779
- to: onto,
780
- })
781
- const ticket_files = await git.filesBetween({
782
- cwd: worktree.path,
783
- from: worktree.base_sha,
784
- to: head,
785
- })
786
- const { conflicts } = await git.rebaseWorktree({
787
- cwd: worktree.path,
788
- from: worktree.base_sha,
789
- to: head,
790
- onto,
791
- })
792
- const tests = testFilesAmong({
793
- files: conflicts,
794
- test_file_patterns: config.test_file_patterns,
795
- })
796
- journal.append({
797
- kind: 'ticket_rebased',
798
- ticket: action.ticket,
799
- role: null,
800
- content: {
801
- cause: action.cause,
802
- base_sha: onto,
803
- tests,
804
- code: conflicts.filter((file) => !tests.includes(file)),
805
- undone,
806
- reinstall: rebaseNeedsInstall({ moved_files, ticket_files }),
807
- },
808
- })
809
- }
810
-
811
- /**
812
- * The config's gates in a worktree (with the install first when a manifest
813
- * changed since its base), journaled as `gates_run`.
814
- */
815
- export const gatesIn = async ({
816
- context,
817
- worktree,
818
- target,
819
- ticket,
820
- }: {
821
- context: BuildContext
822
- worktree: ReplayedWorktree
823
- target: GateTarget
824
- /** `null` for the final review. */
825
- ticket: number | null
826
- }) => {
827
- const { path: cwd, base_sha } = worktree
828
- const result = await runGates({
829
- cwd,
830
- config: context.config,
831
- install: installCommand({
832
- changed_files: difference(
833
- await context.git.changedSince({ cwd, from: base_sha }),
834
- context.state.prepare_made
835
- ),
836
- target,
837
- }),
838
- around_prepare: (run) => watchPrepare({ context, cwd, ticket, run }),
839
- test_files: await testFilesIn({ context, cwd }),
840
- report_file: await reportFile({
841
- context,
842
- ticket,
843
- label: `gates-${target}`,
844
- }),
845
- })
846
- context.journal.append({
847
- kind: 'gates_run',
848
- ticket,
849
- role: null,
850
- content: { target, ...result },
851
- })
852
- }
853
-
854
- /**
855
- * The frozen install in a new (or moved) worktree, journaled as
856
- * `dependencies_installed`. A worktree with no `package.json` has nothing
857
- * to install (`check` is `null`).
858
- */
859
- const installIn = async ({
860
- journal,
861
- cwd,
862
- target,
863
- ticket,
864
- }: {
865
- journal: Journal
866
- cwd: string
867
- target: GateTarget
868
- ticket: number | null
869
- }) => {
870
- const command = newWorktreeInstall({
871
- has_manifest: existsSync(join(cwd, MANIFEST)),
872
- })
873
- journal.append({
874
- kind: 'dependencies_installed',
875
- ticket,
876
- role: null,
877
- content: {
878
- target,
879
- check:
880
- command === null
881
- ? null
882
- : await shellCheck({ name: 'install', command, cwd }),
883
- },
884
- })
885
- }
886
-
887
- /**
888
- * What a build step's executor works with, from the journal as it is now.
889
- *
890
- * @example
891
- * const context = buildContext({ journal, tracker, git, launcher })
892
- */
893
- export const buildContext = ({
894
- journal,
895
- tracker,
896
- git,
897
- launcher,
898
- step,
899
- }: BuildDeps & {
900
- journal: Journal
901
- tracker: Tracker
902
- /** Left out, a first try, numbered after the journal's last record. */
903
- step?: CommentStep
904
- }): BuildContext => {
905
- const records = journal.read()
906
- const state = replayRun({ records })
907
- const run_dir = dirname(journal.file)
908
- return {
909
- git,
910
- launcher,
911
- journal,
912
- tracker,
913
- state,
914
- config: need({ value: state.config, what: 'engine config' }),
915
- run_dir,
916
- step: step ?? {
917
- run_id: basename(run_dir),
918
- first_seq: (records.at(-1)?.seq ?? 0) + 1,
919
- redo: false,
920
- },
921
- }
922
- }
923
-
924
61
  /**
925
62
  * Carries out one build step: git, gates, agents, or the tracker, then
926
63
  * records what happened in the journal. `decide` picks the step.
@@ -935,12 +72,15 @@ export const executeBuildAction = async ({
935
72
  }: BuildDeps & {
936
73
  /**
937
74
  * The final review's steps go to `executeFinalReviewAction`, the
938
- * changeset to `writeChangeset`, and an already-done ticket's to
939
- * `executeAlreadyDoneAction`.
75
+ * changeset to `writeChangeset`, the PR to `openPullRequest`, and an
76
+ * already-done ticket's to `executeAlreadyDoneAction`.
940
77
  */
941
78
  action: Exclude<
942
79
  BuildAction,
943
- FinalReviewAction | AlreadyDoneAction | { type: 'write_changeset' }
80
+ | FinalReviewAction
81
+ | AlreadyDoneAction
82
+ | { type: 'write_changeset' }
83
+ | { type: 'open_pull_request' }
944
84
  >
945
85
  journal: Journal
946
86
  tracker: Tracker
@@ -1040,6 +180,8 @@ export const executeBuildAction = async ({
1040
180
  cwd: path,
1041
181
  commands: bunTestCommands(context),
1042
182
  test_files: await testFilesIn({ context, cwd: path }),
183
+ timeout_ms: testTimeoutOf({ config: context.config }),
184
+ test_concurrency: context.config.test_concurrency,
1043
185
  report_file: await reportFile({
1044
186
  context,
1045
187
  ticket: action.ticket,
@@ -1076,12 +218,21 @@ export const executeBuildAction = async ({
1076
218
  kind: 'worktree_reset',
1077
219
  ticket: action.ticket,
1078
220
  role: null,
1079
- content: { sha },
221
+ content: {
222
+ sha,
223
+ ...(action.bad_test === undefined
224
+ ? {}
225
+ : { bad_test: action.bad_test }),
226
+ },
1080
227
  })
1081
228
  return
1082
229
  }
230
+ case 'send_tests_back':
231
+ return sendTestsBack({ context, action })
1083
232
  case 'run_red_check':
1084
233
  return runRedCheck({ context, action })
234
+ case 'check_already_done':
235
+ return checkAlreadyDoneIn({ context, action })
1085
236
  case 'commit_ticket':
1086
237
  return commitTicket({ context, action })
1087
238
  case 'run_gates':
@@ -1093,6 +244,7 @@ export const executeBuildAction = async ({
1093
244
  : need({ value: state.run_branch, what: 'run branch' }),
1094
245
  target: action.target,
1095
246
  ticket: action.ticket,
247
+ exclusive: action.exclusive,
1096
248
  })
1097
249
  case 'join_run_branch': {
1098
250
  const worktree = ticketWorktree({ state, ticket: action.ticket })
@@ -1154,20 +306,22 @@ export const executeBuildAction = async ({
1154
306
  })
1155
307
  return
1156
308
  }
1157
- case 'undo_join':
309
+ case 'undo_join': {
310
+ const shas = await undoJoin({
311
+ context,
312
+ ticket: action.ticket,
313
+ first_sha: action.first_sha,
314
+ })
315
+ // A refused undo left the run branch alone and the ticket stuck.
316
+ if (shas === null) return
1158
317
  journal.append({
1159
318
  kind: 'join_undone',
1160
319
  ticket: action.ticket,
1161
320
  role: null,
1162
- content: {
1163
- shas: await undoJoin({
1164
- context,
1165
- ticket: action.ticket,
1166
- first_sha: action.first_sha,
1167
- }),
1168
- },
321
+ content: { shas },
1169
322
  })
1170
323
  return
324
+ }
1171
325
  case 'retry_ticket':
1172
326
  return retryTicket({ context, action })
1173
327
  case 'report_stuck':
@@ -1178,7 +332,7 @@ export const executeBuildAction = async ({
1178
332
  case 'report_final_review_stuck':
1179
333
  case 'ship_final_review':
1180
334
  case 'retry_final_review':
1181
- // Tracker-only steps; `executeAction` carries them out.
335
+ // Stuck work's steps; `executeAction` carries them out.
1182
336
  throw new Error(`${action.type} is not a git or agent step.`)
1183
337
  case 'mark_stuck':
1184
338
  // A join crashes cut off too often leaves no half of it behind.
@@ -1198,36 +352,6 @@ export const executeBuildAction = async ({
1198
352
  content: { reason: action.reason, detail: action.detail },
1199
353
  })
1200
354
  return
1201
- case 'open_pull_request': {
1202
- const { head, base, title, body } = action
1203
- // A redo adopts the PR its first try opened before the crash.
1204
- const adopted = context.step.redo
1205
- ? await tracker.findOpenPullRequest({ head })
1206
- : null
1207
- // The spec's open tickets not in the run are named in the PR.
1208
- const full = withNotInRun({
1209
- body,
1210
- open: await openTicketsNotInRun({
1211
- tracker,
1212
- state: context.state,
1213
- }),
1214
- })
1215
- const opened =
1216
- adopted ??
1217
- (await tracker.openPullRequest({
1218
- head,
1219
- base,
1220
- title,
1221
- body: full,
1222
- }))
1223
- journal.append({
1224
- kind: 'pull_request_opened',
1225
- ticket: null,
1226
- role: null,
1227
- content: { ...opened, head, base, title, body: full },
1228
- })
1229
- return
1230
- }
1231
355
  case 'done':
1232
356
  case 'invalid_journal':
1233
357
  return