@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,1060 +1,27 @@
1
- import sortBy from 'lodash/sortBy'
2
-
3
- import {
4
- alreadyDoneStep,
5
- closeTicketStep,
6
- type AlreadyDoneAction,
7
- } from './already-done'
8
- import { changesetStep } from './changeset'
9
- import { crashDetail, crashedOut } from './decide-crashes'
10
- import {
11
- decideFinalReview,
12
- type FinalReviewAction,
13
- } from './decide-final-review'
14
- import {
15
- finalStuckSteps,
16
- replySteps,
17
- skipDependent,
18
- stuckTicketSteps,
19
- type StuckAction,
20
- } from './decide-stuck'
21
- import {
22
- clashFixMessage,
23
- CRASH_SECTION,
24
- failedChecks,
25
- failedTryMessage,
26
- followUpSection,
27
- gateFixMessage,
28
- redFixMessage,
29
- } from './fix-loop-text'
30
- import {
31
- MAX_BAD_TEST_BOUNCES,
32
- MAX_ENGINE_FAILURES,
33
- MAX_FIX_ROUNDS,
34
- MAX_REJOINS,
35
- } from './loop-caps'
36
- import { pullRequestText } from './pull-request-text'
37
- import {
38
- openFindingsText,
39
- reviewFixMessage,
40
- reviewFixSection,
41
- reviewSections,
42
- } from './review-text'
43
- import { retrySection, setupChangeDetail } from './stuck-text'
44
-
45
- import {
46
- prepareSection,
47
- rejoinSection,
48
- rolePrompt,
49
- } from '../agents/role-prompts'
50
- import type { AgentRole, CriterionTests } from '../agents/role-results'
51
- import { endedText } from '../gates/gate-schemas'
52
- import type { TicketSnapshot } from '../intake/intake-schemas'
53
- import type {
54
- ChangesetBump,
55
- CommitStage,
56
- GateTarget,
57
- RejoinCause,
58
- StuckReason,
59
- } from '../journal/journal-record'
60
- import {
61
- EMPTY_TICKET_PROGRESS,
62
- type ReplayedInstall,
63
- type ReplayedRunNote,
64
- type ReplayedSnapshot,
65
- type ReplayedWorktree,
66
- type RunState,
67
- type TicketProgress,
68
- } from '../journal/replay'
69
- import { REFACTOR_LABEL } from '../tracker/tracker'
70
-
1
+ import type { BuildAction } from './build-action'
2
+ import { withChangesetsSections, withWriterSection } from './build-launch'
3
+ import { haltSteps, installSteps } from './decide-build-early'
4
+ import { finishSteps } from './decide-build-ending'
5
+ import { ticketSteps, type TicketWait } from './decide-build-tickets'
6
+ import { buildViewOf } from './decide-build-view'
7
+ import { replySteps } from './decide-stuck'
8
+
9
+ import { featureMapSection, prepareSection } from '../agents/role-prompts'
10
+ import { featureMapPath } from '../config/engine-config'
11
+ import type { RunState } from '../journal/replay'
12
+
13
+ export { isRefactorTicket, type BuildAction } from './build-action'
14
+ export { MAX_RUN_NOTES, mayEditTests, newestRunNotes } from './build-launch'
15
+ export { EMPTY_TEST_REASON } from './decide-red-green'
71
16
  export {
72
17
  MAX_BAD_TEST_BOUNCES,
73
18
  MAX_ENGINE_FAILURES,
74
19
  MAX_FIX_ROUNDS,
75
20
  MAX_REJOINS,
21
+ MAX_TEST_UPDATES,
22
+ MAX_TESTS_PER_CRITERION,
76
23
  } from './loop-caps'
77
24
 
78
- /**
79
- * How many run notes a fresh agent is handed: the newest ones in the run,
80
- * whatever ticket or role wrote them.
81
- */
82
- export const MAX_RUN_NOTES = 10
83
-
84
- /**
85
- * The run notes a fresh agent gets: the `MAX_RUN_NOTES` newest, oldest
86
- * first. The same text written twice counts once, where it was last written.
87
- *
88
- * @example
89
- * newestRunNotes({ run_notes: state.run_notes }) // at most 10, oldest first
90
- */
91
- export const newestRunNotes = ({
92
- run_notes,
93
- }: {
94
- run_notes: ReplayedRunNote[]
95
- }): ReplayedRunNote[] => {
96
- const seen = new Set<string>()
97
- const newest: ReplayedRunNote[] = []
98
- for (const entry of [...run_notes].reverse()) {
99
- if (newest.length === MAX_RUN_NOTES) break
100
- if (seen.has(entry.note)) continue
101
- seen.add(entry.note)
102
- newest.push(entry)
103
- }
104
- return newest.reverse()
105
- }
106
-
107
- /** The next build step for a run whose intake passed. */
108
- export type BuildAction =
109
- /** Make the run branch, in its own worktree, from the base branch. */
110
- | { type: 'create_run_branch'; spec_number: number; base_branch: string }
111
- /** Make the ticket's worktree on a new branch from the run branch. */
112
- | { type: 'create_ticket_worktree'; ticket: number; run_branch: string }
113
- /**
114
- * Install the dependencies from the lockfile, without changing it, in a
115
- * new ticket worktree or the run branch's checkout (`ticket` is `null`),
116
- * before any agent or gate runs there.
117
- */
118
- | {
119
- type: 'install_dependencies'
120
- target: GateTarget
121
- ticket: number | null
122
- }
123
- /** Run the tests before any agent works, to know the old tests. */
124
- | { type: 'run_baseline_tests'; ticket: number }
125
- /**
126
- * Take `from_ticket`'s baseline instead of running the tests again: both
127
- * worktrees start from the same run-branch commit, `base_sha`.
128
- */
129
- | {
130
- type: 'reuse_baseline_tests'
131
- ticket: number
132
- from_ticket: number
133
- base_sha: string
134
- }
135
- /**
136
- * Start a fresh agent session in the ticket's worktree with this prompt.
137
- * `may_edit_tests` is true for the test-writer and for a refactor
138
- * ticket's implementer; the launcher's guards enforce it.
139
- */
140
- | {
141
- type: 'launch_agent'
142
- ticket: number
143
- role: AgentRole
144
- prompt: string
145
- may_edit_tests: boolean
146
- }
147
- /**
148
- * Send a follow-up message, such as a failed check's output, to the agent
149
- * session that is still open, in a fix loop.
150
- */
151
- | {
152
- type: 'follow_up_agent'
153
- ticket: number
154
- role: AgentRole
155
- session_id: string
156
- message: string
157
- }
158
- /**
159
- * Throw away every uncommitted change in the ticket's worktree, so a
160
- * fresh test-writer starts clean after a bad test.
161
- */
162
- | { type: 'reset_ticket_worktree'; ticket: number }
163
- /** Prove every criterion has a test, new tests fail, old tests pass. */
164
- | {
165
- type: 'run_red_check'
166
- ticket: number
167
- criteria_ids: string[]
168
- mapping: CriterionTests[]
169
- }
170
- /** Scan for leftovers, then commit everything in the worktree. */
171
- | {
172
- type: 'commit_ticket'
173
- ticket: number
174
- stage: CommitStage
175
- message: string
176
- }
177
- /** Run the engine config's gates in the ticket worktree or on the run branch. */
178
- | { type: 'run_gates'; ticket: number; target: GateTarget }
179
- /** Replay the approved ticket's commits onto the run branch. */
180
- | { type: 'join_run_branch'; ticket: number }
181
- /**
182
- * Put a joined ticket's whole change back on top of the run branch, as
183
- * uncommitted changes in its worktree, after a clash or failed gates
184
- * after joining. With `undo_first_sha`, first undo the join on the run
185
- * branch, back to before that commit.
186
- */
187
- | {
188
- type: 'rebase_ticket'
189
- ticket: number
190
- cause: RejoinCause
191
- undo_first_sha: string | null
192
- }
193
- /** Remove these git worktrees at the end of the run; branches stay. */
194
- | { type: 'remove_worktrees'; paths: string[] }
195
- /** No ticket can move, and not every ticket has joined. */
196
- | { type: 'invalid_journal'; reason: string }
197
- /** Push the run branch to `origin`. */
198
- | { type: 'push_run_branch'; ticket: number; branch: string }
199
- /** The engine can't safely pick this ticket's next step by itself. */
200
- | {
201
- type: 'mark_stuck'
202
- ticket: number
203
- reason: StuckReason
204
- detail: string
205
- }
206
- /**
207
- * In a repo with changesets, commit the run's one changeset to the run
208
- * branch and push it, before the PR (#461).
209
- */
210
- | {
211
- type: 'write_changeset'
212
- branch: string
213
- bump: ChangesetBump
214
- summary: string
215
- message: string
216
- }
217
- /** Open the run's one pull request from the run branch. */
218
- | {
219
- type: 'open_pull_request'
220
- head: string
221
- base: string
222
- title: string
223
- body: string
224
- }
225
- /** Every ticket joined (or was skipped) and the PR is open. */
226
- | {
227
- type: 'done'
228
- outcome: 'pr_opened'
229
- pull_request: { number: number; url: string }
230
- }
231
- /** The final review of the whole run branch (`decide-final-review.ts`). */
232
- | FinalReviewAction
233
- /** The owner replied `stop`: the run ends without a PR; its branch stays. */
234
- | { type: 'done'; outcome: 'stopped_by_user' }
235
- /** Every ticket was skipped, so there is nothing to open a PR for. */
236
- | { type: 'done'; outcome: 'all_skipped' }
237
- /** Telling the owner about stuck work, and acting on their replies. */
238
- | StuckAction
239
- /** A ticket whose work was already on the base branch (#484). */
240
- | AlreadyDoneAction
241
-
242
- /** Whether a ticket is a refactor ticket: it skips the test-writer and red check. */
243
- export const isRefactorTicket = ({
244
- ticket,
245
- }: {
246
- ticket: TicketSnapshot
247
- }): boolean => ticket.labels.includes(REFACTOR_LABEL)
248
-
249
- /**
250
- * A red commit after a bad-test bounce says so, so the history shows it. A
251
- * refactor ticket's one commit says it is a refactor.
252
- */
253
- const commitMessage = ({
254
- stage,
255
- ticket,
256
- progress,
257
- }: {
258
- stage: CommitStage
259
- ticket: TicketSnapshot
260
- progress: TicketProgress
261
- }): string => {
262
- if (stage === 'fix') {
263
- return `fix: review round ${progress.review_fix?.round ?? progress.review_rounds} for #${ticket.number} ${ticket.title}`
264
- }
265
- if (stage === 'green') {
266
- if (progress.rejoins > 0) {
267
- return `fix: rejoin #${ticket.number} ${ticket.title} onto the run branch`
268
- }
269
- return isRefactorTicket({ ticket })
270
- ? `refactor: #${ticket.number} ${ticket.title}`
271
- : `feat: build #${ticket.number} ${ticket.title}`
272
- }
273
- return progress.bad_test_bounces > 0
274
- ? `test: replace a bad test for #${ticket.number} ${ticket.title}`
275
- : `test: add failing tests for #${ticket.number} ${ticket.title}`
276
- }
277
-
278
- /**
279
- * Whether an agent of this role may edit test files on this ticket: only
280
- * the test-writer, and a refactor ticket's implementer (who may follow
281
- * renames into tests). The engine hands it to the launcher's guards and
282
- * uses it for its own after-turn check, on launches and follow-ups alike.
283
- *
284
- * @example
285
- * mayEditTests({ role: 'implementer', ticket }) // true only on a refactor ticket
286
- */
287
- export const mayEditTests = ({
288
- role,
289
- ticket,
290
- }: {
291
- role: AgentRole
292
- ticket: TicketSnapshot
293
- }): boolean =>
294
- role === 'test-writer' ||
295
- (role === 'implementer' && isRefactorTicket({ ticket }))
296
-
297
- /**
298
- * The sections a launch adds: the review's (see `reviewPromptSections`),
299
- * and, for a test-writer or implementer fixing a ticket on top of the run
300
- * branch, the files that clashed. The reviewer's re-review after a rebase
301
- * is the review's own. The first agent after a `retry` resumed a stuck
302
- * ticket is also told why it got stuck, and the agent taking a turn a
303
- * crash cut off is told of the crash.
304
- */
305
- const promptSections = ({
306
- role,
307
- progress,
308
- }: {
309
- role: AgentRole
310
- progress: TicketProgress
311
- }): string[] => {
312
- const sections = buildPromptSections({ role, progress })
313
- const { retried, crashed_turn } = progress
314
- return [
315
- ...sections,
316
- ...(retried === null ? [] : [retrySection({ retried })]),
317
- ...(crashed_turn === role ? [CRASH_SECTION] : []),
318
- ]
319
- }
320
-
321
- const buildPromptSections = ({
322
- role,
323
- progress,
324
- }: {
325
- role: AgentRole
326
- progress: TicketProgress
327
- }): string[] => {
328
- const review = reviewPromptSections({ role, progress })
329
- const { rejoin } = progress
330
- if (rejoin === null || role === 'ticket-reviewer') return review
331
- // A review fix round after the rejoin has its own findings.
332
- if (progress.commits.green !== null) return review
333
- const section = rejoinSection({ role, rejoin })
334
- return section === null ? review : [...review, section]
335
- }
336
-
337
- /**
338
- * The sections a launch adds for the ticket review: the reviewer's diff and
339
- * gate results (a re-review's new changes and earlier findings), or a
340
- * review fixer's findings while a review fix round is open.
341
- */
342
- const reviewPromptSections = ({
343
- role,
344
- progress,
345
- }: {
346
- role: AgentRole
347
- progress: TicketProgress
348
- }): string[] => {
349
- const fix = progress.review_fix
350
- if (role === 'ticket-reviewer') {
351
- return reviewSections({
352
- progress,
353
- base_sha: progress.worktree?.base_sha ?? 'HEAD',
354
- })
355
- }
356
- if (fix === null || progress.commits.green === null) return []
357
- if (role === 'test-writer' && !fix.tests_answered) {
358
- return [reviewFixSection({ fix, kind: 'test' })]
359
- }
360
- if (role === 'implementer' && !fix.code_answered) {
361
- return [reviewFixSection({ fix, kind: 'code' })]
362
- }
363
- return []
364
- }
365
-
366
- type StepArgs = {
367
- snapshot: ReplayedSnapshot
368
- ticket: TicketSnapshot
369
- progress: TicketProgress
370
- /** The run notes a fresh agent gets (`newestRunNotes`). */
371
- run_notes: ReplayedRunNote[]
372
- }
373
-
374
- /**
375
- * A fresh agent session, handed the run's newest notes. With `follow_up`,
376
- * it stands in for a follow-up whose session is gone (a crash cut it off),
377
- * and its prompt carries the follow-up message.
378
- */
379
- const launch = ({
380
- role,
381
- snapshot,
382
- ticket,
383
- progress,
384
- run_notes,
385
- follow_up,
386
- }: StepArgs & { role: AgentRole; follow_up?: string }): BuildAction => ({
387
- type: 'launch_agent',
388
- ticket: ticket.number,
389
- role,
390
- prompt: rolePrompt({
391
- role,
392
- spec: snapshot.spec,
393
- ticket,
394
- refactor: isRefactorTicket({ ticket }),
395
- bad_test: progress.bad_test,
396
- sections: [
397
- ...promptSections({ role, progress }),
398
- ...(follow_up === undefined
399
- ? []
400
- : [followUpSection({ message: follow_up })]),
401
- ],
402
- run_notes,
403
- }),
404
- may_edit_tests: mayEditTests({ role, ticket }),
405
- })
406
-
407
- const stuck = ({
408
- ticket,
409
- reason,
410
- detail,
411
- }: {
412
- ticket: number
413
- reason: StuckReason
414
- detail: string
415
- }): BuildAction => ({ type: 'mark_stuck', ticket, reason, detail })
416
-
417
- /** Why an install failed, for the stuck detail, or `null` if it passed. */
418
- const installFailure = ({
419
- install,
420
- where,
421
- }: {
422
- install: ReplayedInstall
423
- where: string
424
- }): string | null => {
425
- const { check } = install
426
- if (check === null || check.ok) return null
427
- return `\`${check.command}\` failed in ${where} (${endedText(check)}). Agents never run the install, so fix the manifest or lockfile on the base branch and retry.\n${check.output}`
428
- }
429
-
430
- const commitStep = ({
431
- stage,
432
- ticket,
433
- progress,
434
- }: {
435
- stage: CommitStage
436
- ticket: TicketSnapshot
437
- progress: TicketProgress
438
- }): BuildAction | null => {
439
- if (progress.commits[stage] !== null) return null
440
- const hits = progress.leftovers[stage]
441
- if (hits !== null && hits.length > 0) {
442
- return stuck({
443
- ticket: ticket.number,
444
- reason: 'leftovers_found',
445
- detail: hits
446
- .map(({ path, reason }) => `${path}: ${reason}`)
447
- .join('\n'),
448
- })
449
- }
450
- return {
451
- type: 'commit_ticket',
452
- ticket: ticket.number,
453
- stage,
454
- message: commitMessage({ stage, ticket, progress }),
455
- }
456
- }
457
-
458
- const badTestText = ({ progress }: { progress: TicketProgress }): string => {
459
- const { bad_test } = progress
460
- if (bad_test === null) return 'no reason given'
461
- const where = [bad_test.file, bad_test.name].filter(Boolean).join(' > ')
462
- return where === '' ? bad_test.reason : `${where}: ${bad_test.reason}`
463
- }
464
-
465
- /**
466
- * The test-writer's half of a ticket: fresh tests, the red check and its fix
467
- * loop, then the red commit. `null` once the red commit is made. A ticket
468
- * whose work the test-writer found already on the base branch is marked
469
- * done instead (#484); one that changes no behavior is stuck, as a refactor
470
- * ticket without its label.
471
- */
472
- const testStep = ({
473
- snapshot,
474
- ticket,
475
- progress,
476
- run_notes,
477
- base_branch,
478
- }: StepArgs & { base_branch: string }): BuildAction | null => {
479
- const number = ticket.number
480
- const { test_writer, red_check } = progress
481
- if (test_writer === null) {
482
- return launch({
483
- role: 'test-writer',
484
- snapshot,
485
- ticket,
486
- progress,
487
- run_notes,
488
- })
489
- }
490
- if (test_writer.outcome === 'already_done') {
491
- return alreadyDoneStep({
492
- spec_number: snapshot.spec.number,
493
- base_branch,
494
- ticket,
495
- result: test_writer,
496
- })
497
- }
498
- if (test_writer.outcome === 'nothing_new_to_test') {
499
- return stuck({
500
- ticket: number,
501
- reason: 'nothing_new_to_test',
502
- detail:
503
- `The test-writer says this ticket changes no behavior, so no new test can fail first: ${test_writer.summary || 'no reason given'}\n` +
504
- `A ticket like that is a refactor ticket: add the \`${REFACTOR_LABEL}\` label and retry it.`,
505
- })
506
- }
507
- if (red_check === null) {
508
- return {
509
- type: 'run_red_check',
510
- ticket: number,
511
- criteria_ids: ticket.criteria.map(({ id }) => id),
512
- mapping: test_writer.criteria,
513
- }
514
- }
515
- if (!red_check.ok) {
516
- const problems = red_check.problems.join('\n')
517
- if (progress.red_fix_rounds >= MAX_FIX_ROUNDS) {
518
- return stuck({
519
- ticket: number,
520
- reason: 'red_check_failed',
521
- detail: `The red check still fails after ${MAX_FIX_ROUNDS} fix rounds:\n${problems}`,
522
- })
523
- }
524
- const session_id = progress.sessions['test-writer']
525
- const message = redFixMessage({ red_check })
526
- if (session_id === undefined) {
527
- return launch({
528
- role: 'test-writer',
529
- snapshot,
530
- ticket,
531
- progress,
532
- run_notes,
533
- follow_up: message,
534
- })
535
- }
536
- return {
537
- type: 'follow_up_agent',
538
- ticket: number,
539
- role: 'test-writer',
540
- session_id,
541
- message,
542
- }
543
- }
544
- return commitStep({ stage: 'red', ticket, progress })
545
- }
546
-
547
- /**
548
- * The implementer's half of a ticket: the code, the gates and their fix
549
- * loop, then the green commit. `null` once the green commit is made.
550
- */
551
- const codeStep = ({
552
- snapshot,
553
- ticket,
554
- progress,
555
- run_notes,
556
- }: StepArgs): BuildAction | null => {
557
- const number = ticket.number
558
- const { implementer } = progress
559
- if (implementer === null) {
560
- return launch({
561
- role: 'implementer',
562
- snapshot,
563
- ticket,
564
- progress,
565
- run_notes,
566
- })
567
- }
568
- if (implementer.outcome === 'bad_test') {
569
- if (isRefactorTicket({ ticket })) {
570
- return stuck({
571
- ticket: number,
572
- reason: 'bad_test',
573
- detail: `The implementer of a refactor ticket sent a test back as bad; there is no test-writer to fix it: ${badTestText({ progress })}`,
574
- })
575
- }
576
- if (progress.bad_test_bounces > MAX_BAD_TEST_BOUNCES) {
577
- return stuck({
578
- ticket: number,
579
- reason: 'bad_test',
580
- detail: `The implementer sent a test back as bad a second time: ${badTestText({ progress })}`,
581
- })
582
- }
583
- // Throw away the implementer's work; replay then clears the ticket's
584
- // tests and code, so a fresh test-writer replaces the bad test.
585
- return { type: 'reset_ticket_worktree', ticket: number }
586
- }
587
- return (
588
- gateStep({ snapshot, ticket, progress, run_notes }) ??
589
- commitStep({ stage: 'green', ticket, progress })
590
- )
591
- }
592
-
593
- /**
594
- * The ticket's gates and their fix loop: failed gates go back to the same
595
- * implementer session (a fresh one with the same message if it is gone),
596
- * up to `MAX_FIX_ROUNDS` follow-ups. `null` once the gates pass.
597
- */
598
- const gateStep = ({
599
- snapshot,
600
- ticket,
601
- progress,
602
- run_notes,
603
- }: StepArgs): BuildAction | null => {
604
- const number = ticket.number
605
- const { gates } = progress
606
- if (gates === null) {
607
- return { type: 'run_gates', ticket: number, target: 'ticket' }
608
- }
609
- if (gates.ok) return null
610
- if (progress.gate_fix_rounds >= MAX_FIX_ROUNDS) {
611
- return stuck({
612
- ticket: number,
613
- reason: 'gates_failed',
614
- detail: `The gates still fail after ${MAX_FIX_ROUNDS} fix rounds:\n${failedChecks({ gates })}`,
615
- })
616
- }
617
- const session_id = progress.sessions.implementer
618
- const message = gateFixMessage({ gates })
619
- if (session_id === undefined) {
620
- return launch({
621
- role: 'implementer',
622
- snapshot,
623
- ticket,
624
- progress,
625
- run_notes,
626
- follow_up: message,
627
- })
628
- }
629
- return {
630
- type: 'follow_up_agent',
631
- ticket: number,
632
- role: 'implementer',
633
- session_id,
634
- message,
635
- }
636
- }
637
-
638
- /**
639
- * The ticket review and its fix loop, after the green commit. A fresh
640
- * reviewer checks the committed diff. Blockers and should-fixes open a fix
641
- * round: test findings go to a fresh test-writer first, then code findings
642
- * to the same implementer session (a fresh one if it is gone). The fixes
643
- * pass the gates, get their own commit, and a fresh reviewer checks only
644
- * the new changes. A review that still asks for changes after
645
- * `MAX_FIX_ROUNDS` fix rounds is stuck. `null` once a review approves.
646
- */
647
- const reviewStep = ({
648
- snapshot,
649
- ticket,
650
- progress,
651
- run_notes,
652
- }: StepArgs): BuildAction | null => {
653
- const number = ticket.number
654
- const reviewer = () =>
655
- launch({
656
- role: 'ticket-reviewer',
657
- snapshot,
658
- ticket,
659
- progress,
660
- run_notes,
661
- })
662
- if (progress.review === null) return reviewer()
663
- const fix = progress.review_fix
664
- if (fix === null) return null
665
- if (fix.round > MAX_FIX_ROUNDS) {
666
- return stuck({
667
- ticket: number,
668
- reason: 'changes_requested',
669
- detail: `The ticket review still asks for changes after ${MAX_FIX_ROUNDS} fix rounds:\n${openFindingsText({ findings: fix.findings })}`,
670
- })
671
- }
672
- if (!fix.tests_answered) {
673
- return launch({
674
- role: 'test-writer',
675
- snapshot,
676
- ticket,
677
- progress,
678
- run_notes,
679
- })
680
- }
681
- if (!fix.code_answered) {
682
- const session_id = progress.sessions.implementer
683
- if (session_id === undefined) {
684
- return launch({
685
- role: 'implementer',
686
- snapshot,
687
- ticket,
688
- progress,
689
- run_notes,
690
- })
691
- }
692
- return {
693
- type: 'follow_up_agent',
694
- ticket: number,
695
- role: 'implementer',
696
- session_id,
697
- message: reviewFixMessage({ fix }),
698
- }
699
- }
700
- if (fix.bad_test !== null) {
701
- return stuck({
702
- ticket: number,
703
- reason: 'bad_test',
704
- detail: `While fixing review findings, the implementer sent a test back as bad: ${badTestText({ progress: { ...progress, bad_test: fix.bad_test } })}`,
705
- })
706
- }
707
- return (
708
- gateStep({ snapshot, ticket, progress, run_notes }) ??
709
- commitStep({ stage: 'fix', ticket, progress }) ??
710
- reviewer()
711
- )
712
- }
713
-
714
- /**
715
- * The next step after a failed agent turn. An engine failure starts a fresh
716
- * agent of the same role without using up a try, until
717
- * `MAX_ENGINE_FAILURES` in a row. Any other failure uses up one of the
718
- * role's tries on the ticket; with tries left, a test-writer or implementer
719
- * gets a follow-up in the session that failed (a fresh launch with the same
720
- * message if it has none, such as after a crash), and a reviewer a fresh
721
- * launch. The last try's failure is stuck.
722
- */
723
- const failedTurnStep = ({
724
- snapshot,
725
- ticket,
726
- progress,
727
- run_notes,
728
- failed,
729
- }: StepArgs & {
730
- failed: NonNullable<TicketProgress['agent_failure']>
731
- }): BuildAction => {
732
- const { role, error, failure, session_id } = failed
733
- if (failure === 'engine') {
734
- if (progress.engine_failures >= MAX_ENGINE_FAILURES) {
735
- return stuck({
736
- ticket: ticket.number,
737
- reason: 'agent_failed',
738
- detail: `The engine failed to run the ${role} ${progress.engine_failures} times in a row: ${error}`,
739
- })
740
- }
741
- return launch({ role, snapshot, ticket, progress, run_notes })
742
- }
743
- const tries = progress.failed_tries[role] ?? 0
744
- if (tries >= MAX_FIX_ROUNDS) {
745
- return stuck({
746
- ticket: ticket.number,
747
- reason: 'agent_failed',
748
- detail: `The ${role} failed ${tries} tries; the last one: ${error}`,
749
- })
750
- }
751
- if (role === 'ticket-reviewer') {
752
- return launch({ role, snapshot, ticket, progress, run_notes })
753
- }
754
- const message = failedTryMessage({ failure, error })
755
- if (session_id === null) {
756
- return launch({
757
- role,
758
- snapshot,
759
- ticket,
760
- progress,
761
- run_notes,
762
- follow_up: message,
763
- })
764
- }
765
- return {
766
- type: 'follow_up_agent',
767
- ticket: ticket.number,
768
- role,
769
- session_id,
770
- message,
771
- }
772
- }
773
-
774
- /**
775
- * The fix on top of the run branch after a rebase: first a fresh test-writer
776
- * for clashed tests, then the implementer for clashed code. `null` once both
777
- * have answered; the gates, the green commit, and a fresh review of the new
778
- * changes follow as usual. A bad test here is stuck: resetting the worktree
779
- * would throw away the ticket's uncommitted change.
780
- */
781
- const rejoinStep = ({
782
- snapshot,
783
- ticket,
784
- progress,
785
- run_notes,
786
- }: StepArgs): BuildAction | null => {
787
- const { rejoin } = progress
788
- if (rejoin === null) return null
789
- if (progress.implementer?.outcome === 'bad_test') {
790
- return stuck({
791
- ticket: ticket.number,
792
- reason: 'bad_test',
793
- detail: `The implementer sent a test back as bad while fixing the ticket on top of the run branch; resetting the worktree would throw the ticket's change away: ${badTestText({ progress })}`,
794
- })
795
- }
796
- if (rejoin.tests_pending) {
797
- return launch({
798
- role: 'test-writer',
799
- snapshot,
800
- ticket,
801
- progress,
802
- run_notes,
803
- })
804
- }
805
- if (rejoin.code_pending) {
806
- const session_id = progress.sessions.implementer
807
- if (session_id === undefined) {
808
- return launch({
809
- role: 'implementer',
810
- snapshot,
811
- ticket,
812
- progress,
813
- run_notes,
814
- })
815
- }
816
- return {
817
- type: 'follow_up_agent',
818
- ticket: ticket.number,
819
- role: 'implementer',
820
- session_id,
821
- message: clashFixMessage({ rejoin }),
822
- }
823
- }
824
- return null
825
- }
826
-
827
- /**
828
- * The join section of an approved ticket, for the first ticket in the join
829
- * queue only: join, the gates on the run branch, the push. A clash or failed
830
- * gates sends the ticket back onto the run branch, up to `MAX_REJOINS`
831
- * times; after that it is stuck. `null` once pushed.
832
- */
833
- const joinStep = ({
834
- ticket,
835
- progress,
836
- run_branch,
837
- }: {
838
- ticket: TicketSnapshot
839
- progress: TicketProgress
840
- run_branch: ReplayedWorktree
841
- }): BuildAction | null => {
842
- const number = ticket.number
843
- const { joined, join_gates } = progress
844
- if (joined === null) {
845
- return { type: 'join_run_branch', ticket: number }
846
- }
847
- if (!joined.ok) {
848
- if (progress.rejoins >= MAX_REJOINS) {
849
- return stuck({
850
- ticket: number,
851
- reason: 'join_failed',
852
- detail: `The ticket still clashes with the run branch after ${MAX_REJOINS} rebases:\n${joined.error}`,
853
- })
854
- }
855
- return {
856
- type: 'rebase_ticket',
857
- ticket: number,
858
- cause: 'clash',
859
- undo_first_sha: null,
860
- }
861
- }
862
- if (join_gates === null) {
863
- return { type: 'run_gates', ticket: number, target: 'run_branch' }
864
- }
865
- if (!join_gates.ok) {
866
- if (progress.rejoins >= MAX_REJOINS) {
867
- return stuck({
868
- ticket: number,
869
- reason: 'join_gates_failed',
870
- detail: `The gates still fail after joining, after ${MAX_REJOINS} rebases:\n${failedChecks({ gates: join_gates })}`,
871
- })
872
- }
873
- return {
874
- type: 'rebase_ticket',
875
- ticket: number,
876
- cause: 'join_gates',
877
- undo_first_sha: joined.shas[0] ?? null,
878
- }
879
- }
880
- if (progress.pushed === null) {
881
- return {
882
- type: 'push_run_branch',
883
- ticket: number,
884
- branch: run_branch.branch,
885
- }
886
- }
887
- return null
888
- }
889
-
890
- /**
891
- * How a ticket with no baseline gets one. Tickets whose worktrees start from
892
- * the same run-branch commit share one baseline test run: a ticket reuses a
893
- * baseline taken at its commit, or else the first such ticket still able to
894
- * move runs the tests and the rest wait for it. `null`: it waits.
895
- */
896
- const sharedBaselineStep = ({
897
- number,
898
- numbers,
899
- progress,
900
- can_move,
901
- }: {
902
- number: number
903
- /** The run's tickets, in order. */
904
- numbers: number[]
905
- progress: (number: number) => TicketProgress
906
- /** Whether a ticket is neither pushed, skipped, nor stuck. */
907
- can_move: (number: number) => boolean
908
- }): BuildAction | null => {
909
- const base_sha = progress(number).worktree?.base_sha
910
- if (base_sha === undefined) return null
911
- const lender = numbers.find(
912
- (other) =>
913
- other !== number &&
914
- progress(other).baseline !== null &&
915
- progress(other).baseline_sha === base_sha
916
- )
917
- if (lender !== undefined) {
918
- return {
919
- type: 'reuse_baseline_tests',
920
- ticket: number,
921
- from_ticket: lender,
922
- base_sha,
923
- }
924
- }
925
- const runner = numbers.find(
926
- (other) =>
927
- can_move(other) &&
928
- progress(other).baseline === null &&
929
- progress(other).worktree?.base_sha === base_sha
930
- )
931
- return runner === number
932
- ? { type: 'run_baseline_tests', ticket: number }
933
- : null
934
- }
935
-
936
- /**
937
- * The next step for one ticket that has its worktree, or `null` when it has
938
- * nothing to do now: it pushed, or its review approved and it waits its turn
939
- * to join.
940
- */
941
- const nextTicketStep = ({
942
- snapshot,
943
- ticket,
944
- progress,
945
- run_notes,
946
- run_branch,
947
- joiner,
948
- setup_files,
949
- baseline_step,
950
- base_branch,
951
- }: StepArgs & {
952
- run_branch: ReplayedWorktree
953
- /** The branch the run started from. */
954
- base_branch: string
955
- /** Whether the ticket is first in the join queue. */
956
- joiner: boolean
957
- /** The config's test setup files. */
958
- setup_files: string[]
959
- /** How it gets its baseline, if it has none (`sharedBaselineStep`). */
960
- baseline_step: BuildAction | null
961
- }): BuildAction | null => {
962
- const number = ticket.number
963
- if (progress.agent_failure !== null) {
964
- return failedTurnStep({
965
- snapshot,
966
- ticket,
967
- progress,
968
- run_notes,
969
- failed: progress.agent_failure,
970
- })
971
- }
972
- if (progress.setup_change !== null) {
973
- return stuck({
974
- ticket: number,
975
- reason: 'setup_change_needed',
976
- detail: setupChangeDetail({
977
- ...progress.setup_change,
978
- setup_files,
979
- }),
980
- })
981
- }
982
- if (progress.install === null) {
983
- return {
984
- type: 'install_dependencies',
985
- target: 'ticket',
986
- ticket: number,
987
- }
988
- }
989
- const ticketFailure = installFailure({
990
- install: progress.install,
991
- where: `the worktree of #${number}`,
992
- })
993
- if (ticketFailure !== null) {
994
- return stuck({
995
- ticket: number,
996
- reason: 'install_failed',
997
- detail: ticketFailure,
998
- })
999
- }
1000
- const { prepare } = progress
1001
- if (prepare !== null && !prepare.ok) {
1002
- return stuck({
1003
- ticket: number,
1004
- reason: 'prepare_failed',
1005
- detail: `\`${prepare.command}\` failed in the worktree of #${number} (${endedText(prepare)}) before any agent worked on it. Agents never run the prepare command, so fix what it needs (a missing tool, say) and retry.\n${prepare.output}`,
1006
- })
1007
- }
1008
- if (progress.baseline === null) return baseline_step
1009
- if (progress.commits.green === null) {
1010
- const rejoin = rejoinStep({
1011
- snapshot,
1012
- ticket,
1013
- progress,
1014
- run_notes,
1015
- })
1016
- if (rejoin !== null) return rejoin
1017
- const tests = isRefactorTicket({ ticket })
1018
- ? null
1019
- : testStep({
1020
- snapshot,
1021
- ticket,
1022
- progress,
1023
- run_notes,
1024
- base_branch,
1025
- })
1026
- if (tests !== null) return tests
1027
- const code = codeStep({ snapshot, ticket, progress, run_notes })
1028
- if (code !== null) return code
1029
- }
1030
- const review = reviewStep({
1031
- snapshot,
1032
- ticket,
1033
- progress,
1034
- run_notes,
1035
- })
1036
- if (review !== null) return review
1037
- return joiner ? joinStep({ ticket, progress, run_branch }) : null
1038
- }
1039
-
1040
- /**
1041
- * Whether a ticket's review finally approved it (no fix round open) and it
1042
- * has not pushed yet: it waits in the join queue, or is joining.
1043
- */
1044
- const inJoinQueue = (progress: TicketProgress): boolean =>
1045
- progress.approved_seq !== null &&
1046
- progress.review_fix === null &&
1047
- progress.pushed === null
1048
-
1049
- /** The worktrees not removed yet, of these paths. */
1050
- const notRemoved = ({
1051
- paths,
1052
- state,
1053
- }: {
1054
- paths: string[]
1055
- state: RunState
1056
- }): string[] => paths.filter((path) => !state.removed_worktrees.includes(path))
1057
-
1058
25
  /**
1059
26
  * The build half of the decision step: every step of building the run's
1060
27
  * tickets that can run now, at most one per ticket, then the PR. Pure.
@@ -1063,13 +30,16 @@ const notRemoved = ({
1063
30
  * the run branch's tip, then its install) once every ticket it waits on has
1064
31
  * pushed, and while no ticket waits to join, so it never builds on joined
1065
32
  * commits whose gates have not passed yet. Approved tickets join one at a
1066
- * time, in the order their reviews finally approved them. A clash or failed
33
+ * time, in the order their reviews finally approved them, except that one
34
+ * already joining finishes first. A clash or failed
1067
35
  * gates after joining puts the ticket's change back on top of the run
1068
- * branch to be fixed there (up to `MAX_REJOINS` times), then re-reviewed. A
1069
- * stuck ticket ends the run: no ticket starts or moves on, and the worktrees
1070
- * of pushed tickets are removed. Once every ticket pushed, the final review
1071
- * looks at the whole run branch (`decideFinalReview`) before the PR opens; a
1072
- * stuck final review ends the run with the run branch's worktree kept. In a
36
+ * branch to be fixed there (up to `MAX_REJOINS` times), then re-reviewed.
37
+ * While a stuck ticket's join is still on the run branch, no ticket starts
38
+ * or joins until it is undone (#519), so no join lands on top of a join
39
+ * that is going away. A stuck ticket ends the run: no ticket starts or
40
+ * moves on, and the worktrees of pushed tickets are removed. Once every
41
+ * ticket pushed, the final review looks at the whole run branch
42
+ * (`decideFinalReview`) before the PR opens; a stuck final review ends the run with the run branch's worktree kept. In a
1073
43
  * repo with changesets, the run's changeset is written just before the PR
1074
44
  * (`write_changeset`), once. Once
1075
45
  * the PR is open, every worktree is removed. Branches and the journal stay.
@@ -1079,12 +49,17 @@ const notRemoved = ({
1079
49
  * up to `MAX_FIX_ROUNDS` follow-ups; then the ticket is stuck. A bad test
1080
50
  * throws away the implementer's work and goes to a fresh test-writer, whose
1081
51
  * tests get their own red check and red commit; the next bad test is stuck.
1082
- * "Nothing new to test" is stuck at once. A refactor ticket skips the
1083
- * test-writer and the red check. The ticket review's findings go back for
1084
- * fixing in capped rounds, each re-reviewed.
52
+ * After a rebase, a bad test can't throw the work away: the tests go back
53
+ * to the test-writer to fit the run branch, with the code kept, up to
54
+ * `MAX_TEST_UPDATES` times (#489). "Nothing new to test" is stuck at once.
55
+ * A refactor ticket skips the test-writer and the red check. The ticket
56
+ * review's findings go back for fixing in capped rounds, each re-reviewed.
1085
57
  *
1086
58
  * Already done (#484): a ticket whose test-writer found its work already on
1087
- * the base branch is marked done (`mark_already_done`), not stuck. It counts
59
+ * the base branch has that evidence checked first (`check_already_done`,
60
+ * #495); evidence that doesn't check out is a failed try for the
61
+ * test-writer. Once it checks out, the ticket is marked done
62
+ * (`mark_already_done`), not stuck. It counts
1088
63
  * as pushed for the tickets that wait on it, and the PR closes it. With no
1089
64
  * ticket pushed, the engine closes the already-done tickets itself
1090
65
  * (`close_ticket`); if every ticket was already done, the run ends with
@@ -1100,334 +75,92 @@ const notRemoved = ({
1100
75
  * decideBuild({ state, spec_number })
1101
76
  * // [{ type: 'create_ticket_worktree', ticket: 11, ... }, { type: 'create_ticket_worktree', ticket: 12, ... }]
1102
77
  */
1103
- export const decideBuild = ({
78
+ export const decideBuild = (args: {
79
+ state: RunState
80
+ spec_number: number
81
+ }): BuildAction[] => decideBuildPlan(args).steps
82
+
83
+ /**
84
+ * `decideBuild`'s steps, and beside them the tickets that wait on purpose
85
+ * between steps now (#559): the same decision, so a wait is only ever one
86
+ * that left its ticket no step. None while the run is halted, holds on its
87
+ * install, or has yet to make its run branch.
88
+ *
89
+ * @example
90
+ * decideBuildPlan({ state, spec_number })
91
+ * // { steps: [{ type: 'run_baseline_tests', ticket: 11 }], waits: [{ ticket: 12, for: 'baseline', on_ticket: 11 }] }
92
+ */
93
+ export const decideBuildPlan = ({
1104
94
  state,
1105
95
  spec_number,
1106
96
  }: {
1107
97
  state: RunState
1108
98
  spec_number: number
1109
- }): BuildAction[] => {
1110
- const steps = buildSteps({ state, spec_number })
99
+ }): { steps: BuildAction[]; waits: TicketWait[] } => {
100
+ const { steps, waits } = buildSteps({ state, spec_number })
1111
101
  const command = state.config?.prepare
1112
- return command === undefined
1113
- ? steps
1114
- : steps.map((step) => withPrepareSection({ step, command }))
102
+ const prepared =
103
+ command === undefined
104
+ ? steps
105
+ : steps.map((step) =>
106
+ withWriterSection({
107
+ step,
108
+ section: prepareSection({ command }),
109
+ })
110
+ )
111
+ const map =
112
+ state.config === null ? null : featureMapPath({ config: state.config })
113
+ const mapped =
114
+ map === null
115
+ ? prepared
116
+ : prepared.map((step) =>
117
+ withWriterSection({
118
+ step,
119
+ section: featureMapSection({ path: map }),
120
+ only: 'test-writer',
121
+ })
122
+ )
123
+ return {
124
+ steps: state.changesets ? mapped.map(withChangesetsSections) : mapped,
125
+ waits,
126
+ }
1115
127
  }
1116
128
 
1117
129
  /**
1118
- * A fresh test-writer's or implementer's launch (a ticket's or the final
1119
- * review's fixers), with the repo's prepare command named in its prompt.
1120
- * Any other step as it is.
130
+ * `decideBuild`'s steps, before the prepare command joins any prompt: the run
131
+ * branch, then the steps that end the run or hold it on the run branch's
132
+ * install, each ticket's next step, and last the finish (`finishSteps`);
133
+ * with the tickets' waits once they are decided.
1121
134
  */
1122
- const withPrepareSection = ({
1123
- step,
1124
- command,
1125
- }: {
1126
- step: BuildAction
1127
- command: string
1128
- }): BuildAction => {
1129
- const writesCode =
1130
- (step.type === 'launch_agent' || step.type === 'launch_final_fixer') &&
1131
- (step.role === 'test-writer' || step.role === 'implementer')
1132
- return writesCode
1133
- ? {
1134
- ...step,
1135
- prompt: `${step.prompt}\n\n${prepareSection({ command })}`,
1136
- }
1137
- : step
1138
- }
1139
-
1140
- /** `decideBuild`'s steps, before the prepare command joins any prompt. */
1141
135
  const buildSteps = ({
1142
136
  state,
1143
137
  spec_number,
1144
138
  }: {
1145
139
  state: RunState
1146
140
  spec_number: number
1147
- }): BuildAction[] => {
1148
- const { snapshot, run_branch, run_branch_install, pull_request } = state
1149
- const base_branch = state.base_branch ?? 'main'
1150
- const run_notes = newestRunNotes({ run_notes: state.run_notes })
141
+ }): { steps: BuildAction[]; waits: TicketWait[] } => {
142
+ const { snapshot, run_branch } = state
1151
143
  if (run_branch === null || snapshot === null) {
1152
- return [{ type: 'create_run_branch', spec_number, base_branch }]
1153
- }
1154
- const numbers = snapshot.ticket_order.filter(
1155
- (number) => snapshot.tickets[number] !== undefined
1156
- )
1157
- const progress = (number: number): TicketProgress =>
1158
- state.tickets[number] ?? EMPTY_TICKET_PROGRESS
1159
-
1160
- const skipped = (number: number) => progress(number).skipped !== null
1161
- const pushed = (number: number) =>
1162
- !numbers.includes(number) || progress(number).pushed !== null
1163
- const alreadyDone = (number: number) =>
1164
- progress(number).already_done !== null
1165
- /** Pushed, or its work was already on the base branch (#484). */
1166
- const finished = (number: number) => pushed(number) || alreadyDone(number)
1167
-
1168
- if (state.stop !== null) {
1169
- // Nothing new starts; finished tickets' worktrees go, the rest stay.
1170
- const paths = notRemoved({
1171
- state,
1172
- paths: numbers.flatMap((number) => {
1173
- const { worktree } = progress(number)
1174
- return finished(number) && worktree !== null
1175
- ? [worktree.path]
1176
- : []
1177
- }),
1178
- })
1179
- if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
1180
- return [{ type: 'done', outcome: 'stopped_by_user' }]
1181
- }
1182
-
1183
- if (pull_request !== null) {
1184
- const paths = notRemoved({
1185
- state,
1186
- paths: [
1187
- ...numbers.flatMap((number) => {
1188
- const { worktree } = progress(number)
1189
- return worktree === null ? [] : [worktree.path]
1190
- }),
1191
- run_branch.path,
1192
- ],
1193
- })
1194
- if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
1195
- return [{ type: 'done', outcome: 'pr_opened', pull_request }]
1196
- }
1197
-
1198
- if (run_branch_install === null) {
1199
- return [
1200
- {
1201
- type: 'install_dependencies',
1202
- target: 'run_branch',
1203
- ticket: null,
1204
- },
1205
- ]
1206
- }
1207
- const runBranchFailure = installFailure({
1208
- install: run_branch_install,
1209
- where: "the run branch's checkout",
1210
- })
1211
- const replies = replySteps({ state, spec_number, numbers })
1212
- const stuckSteps = (number: number): BuildAction[] => {
1213
- const ticket = snapshot.tickets[number]
1214
- return ticket === undefined
1215
- ? []
1216
- : stuckTicketSteps({
1217
- spec_number,
1218
- ticket,
1219
- progress: progress(number),
1220
- })
1221
- }
1222
- const isStuck = (number: number) =>
1223
- progress(number).stuck !== null && !skipped(number)
1224
- if (runBranchFailure !== null) {
1225
- // Nothing builds on a run branch whose install failed: the first
1226
- // ticket is stuck on it until a retry installs it again.
1227
- if (numbers.some(isStuck)) {
1228
- return [...numbers.filter(isStuck).flatMap(stuckSteps), ...replies]
1229
- }
1230
- const first = numbers.find(
1231
- (number) => !finished(number) && !skipped(number)
1232
- )
1233
- if (first !== undefined) {
1234
- return [
1235
- stuck({
1236
- ticket: first,
1237
- reason: 'install_failed',
1238
- detail: runBranchFailure,
1239
- }),
1240
- ]
1241
- }
1242
- }
1243
-
1244
- const queue = sortBy(
1245
- numbers.filter(
1246
- (number) =>
1247
- inJoinQueue(progress(number)) &&
1248
- !isStuck(number) &&
1249
- !skipped(number)
1250
- ),
1251
- (number) => progress(number).approved_seq
1252
- )
1253
- const joiner = queue[0] ?? null
1254
- // A stuck ticket's join is undone before any new ticket starts on it.
1255
- const stuckJoinOnRunBranch = numbers.some(
1256
- (number) =>
1257
- isStuck(number) &&
1258
- progress(number).joined?.ok === true &&
1259
- progress(number).pushed === null
1260
- )
1261
-
1262
- const steps = numbers.flatMap((number): BuildAction[] => {
1263
- const ticket = snapshot.tickets[number]
1264
- const ticketProgress = progress(number)
1265
- if (ticket === undefined || finished(number)) return []
1266
- if (skipped(number)) return []
1267
- // The same step cut off by crashes too often is not taken again.
1268
- const crash = crashedOut({
1269
- crashes: state.crashes,
1270
- keys: (key) => key === String(number),
1271
- })
1272
- if (crash !== null) {
1273
- return [
1274
- stuck({
1275
- ticket: number,
1276
- reason: 'crashed',
1277
- detail: crashDetail({ crash }),
1278
- }),
1279
- ]
1280
- }
1281
- if (isStuck(number)) return stuckSteps(number)
1282
- const skippedBlocker = ticket.blockers.find(skipped)
1283
- if (skippedBlocker !== undefined) {
1284
- return [
1285
- skipDependent({
1286
- ticket: number,
1287
- because: skippedBlocker,
1288
- spec_number,
1289
- }),
1290
- ]
1291
- }
1292
- if (ticketProgress.worktree === null) {
1293
- const ready =
1294
- queue.length === 0 &&
1295
- !stuckJoinOnRunBranch &&
1296
- ticket.blockers.every(finished)
1297
- return ready
1298
- ? [
1299
- {
1300
- type: 'create_ticket_worktree',
1301
- ticket: number,
1302
- run_branch: run_branch.branch,
1303
- },
1304
- ]
1305
- : []
1306
- }
1307
- const step = nextTicketStep({
1308
- snapshot,
1309
- ticket,
1310
- progress: ticketProgress,
1311
- run_notes,
1312
- run_branch,
1313
- base_branch,
1314
- joiner: number === joiner,
1315
- setup_files: state.config?.test_setup_files ?? [],
1316
- baseline_step:
1317
- ticketProgress.baseline === null
1318
- ? sharedBaselineStep({
1319
- number,
1320
- numbers,
1321
- progress,
1322
- can_move: (other) =>
1323
- !finished(other) &&
1324
- !skipped(other) &&
1325
- !isStuck(other),
1326
- })
1327
- : null,
1328
- })
1329
- return step === null ? [] : [step]
1330
- })
1331
- if (steps.length > 0 || replies.length > 0) return [...steps, ...replies]
1332
-
1333
- if (
1334
- numbers.every((number) => finished(number) || skipped(number)) &&
1335
- !numbers.some(pushed)
1336
- ) {
1337
- // No ticket joined the run branch: no PR, and nothing left to build.
1338
- // The engine closes the already-done tickets itself (#484).
1339
- const closing = numbers.filter(
1340
- (number) => alreadyDone(number) && !progress(number).closed
1341
- )
1342
- if (closing.length > 0) {
1343
- return closing.map((number) =>
1344
- closeTicketStep({
144
+ return {
145
+ steps: [
146
+ {
147
+ type: 'create_run_branch',
1345
148
  spec_number,
1346
- base_branch,
1347
- ticket: number,
1348
- shas: progress(number).already_done?.shas ?? [],
1349
- })
1350
- )
1351
- }
1352
- const paths = notRemoved({
1353
- state,
1354
- paths: [
1355
- ...numbers.flatMap((number) => {
1356
- const { worktree } = progress(number)
1357
- return worktree === null ? [] : [worktree.path]
1358
- }),
1359
- run_branch.path,
149
+ base_branch: state.base_branch ?? 'main',
150
+ },
1360
151
  ],
1361
- })
1362
- if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
1363
- if (numbers.some(skipped)) {
1364
- return [{ type: 'done', outcome: 'all_skipped' }]
152
+ waits: [],
1365
153
  }
1366
- // Every ticket's work was already done: nothing to do.
1367
- return [
1368
- {
1369
- type: 'finish_nothing_to_do',
1370
- closed_tickets: snapshot.closed_tickets,
1371
- already_done: numbers.filter(alreadyDone),
1372
- },
1373
- ]
1374
154
  }
1375
- if (numbers.every((number) => finished(number) || skipped(number))) {
1376
- const final = decideFinalReview({
1377
- review: state.final_review,
1378
- crashes: state.crashes,
1379
- snapshot,
1380
- run_branch,
1381
- run_branch_gates: state.run_branch_gates,
1382
- run_notes,
1383
- })
1384
- if (final.status === 'working') return final.actions
1385
- if (final.status === 'stuck') {
1386
- // The run branch's worktree stays for a retry.
1387
- const paths = notRemoved({
1388
- state,
1389
- paths: numbers.flatMap((number) => {
1390
- const { worktree } = progress(number)
1391
- return worktree === null ? [] : [worktree.path]
1392
- }),
1393
- })
1394
- if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
1395
- // The owner hears of it, and the run waits for their reply.
1396
- return [
1397
- ...finalStuckSteps({ state, spec_number, run_branch }),
1398
- ...replies,
1399
- ]
1400
- }
1401
- if (state.changesets && state.changeset === null) {
1402
- return [
1403
- {
1404
- type: 'write_changeset',
1405
- branch: run_branch.branch,
1406
- ...changesetStep({ spec: snapshot.spec }),
1407
- },
1408
- ]
1409
- }
1410
- return [
1411
- {
1412
- type: 'open_pull_request',
1413
- head: run_branch.branch,
1414
- base: base_branch,
1415
- ...pullRequestText({
1416
- snapshot,
1417
- tickets: state.tickets,
1418
- final_review: state.final_review,
1419
- memory_saves: state.memory.saved?.saves ?? [],
1420
- }),
1421
- },
1422
- ]
155
+ const view = buildViewOf({ state, spec_number, snapshot, run_branch })
156
+ const halted = haltSteps({ state, view })
157
+ if (halted !== null) return { steps: halted, waits: [] }
158
+ const replies = replySteps({ state, spec_number, numbers: view.numbers })
159
+ const installing = installSteps({ state, view, replies })
160
+ if (installing !== null) return { steps: installing, waits: [] }
161
+ const { steps, waits } = ticketSteps({ state, view })
162
+ if (steps.length > 0 || replies.length > 0) {
163
+ return { steps: [...steps, ...replies], waits }
1423
164
  }
1424
- return [
1425
- {
1426
- type: 'invalid_journal',
1427
- reason: `No ticket can move: ${numbers
1428
- .filter((number) => !finished(number))
1429
- .map((number) => `#${number}`)
1430
- .join(', ')} wait on tickets that never join.`,
1431
- },
1432
- ]
165
+ return { steps: finishSteps({ state, view, replies }), waits }
1433
166
  }