@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
@@ -0,0 +1,398 @@
1
+ /**
2
+ * A ticket's test and code steps: the test-writer's tests, the red check and
3
+ * red commit, then the implementer's code, the gates and the green commit.
4
+ */
5
+
6
+ import { alreadyDoneStep, checkAlreadyDoneStep } from './already-done'
7
+ import {
8
+ isRefactorTicket,
9
+ stuck,
10
+ type BuildAction,
11
+ type StepArgs,
12
+ } from './build-action'
13
+ import { launch } from './build-launch'
14
+ import { environmentFailedDetail } from './environment-rerun'
15
+ import { failedChecks, gateFixMessage, redFixMessage } from './fix-loop-text'
16
+ import { MAX_BAD_TEST_BOUNCES, MAX_FIX_ROUNDS } from './loop-caps'
17
+ import { badTestDetail, badTestTwiceText } from './stuck-text'
18
+
19
+ import { findCase } from '../gates/red-check'
20
+ import type { TicketSnapshot } from '../intake/intake-schemas'
21
+ import type { CommitStage } from '../journal/journal-record'
22
+ import type { TicketProgress } from '../journal/replay'
23
+ import type {
24
+ LoneRerunCheck,
25
+ SharedFailureFields,
26
+ } from '../journal/ticket-progress'
27
+ import { REFACTOR_LABEL } from '../tracker/tracker'
28
+
29
+ /**
30
+ * A red commit after a bad-test bounce says so, so the history shows it. A
31
+ * refactor ticket's one commit says it is a refactor.
32
+ */
33
+ const commitMessage = ({
34
+ stage,
35
+ ticket,
36
+ progress,
37
+ }: {
38
+ stage: CommitStage
39
+ ticket: TicketSnapshot
40
+ progress: TicketProgress
41
+ }): string => {
42
+ if (stage === 'fix') {
43
+ return `fix: review round ${progress.review_fix?.round ?? progress.review_rounds} for #${ticket.number} ${ticket.title}`
44
+ }
45
+ if (stage === 'green') {
46
+ if (progress.rejoins > 0) {
47
+ return `fix: rejoin #${ticket.number} ${ticket.title} onto the run branch`
48
+ }
49
+ return isRefactorTicket({ ticket })
50
+ ? `refactor: #${ticket.number} ${ticket.title}`
51
+ : `feat: build #${ticket.number} ${ticket.title}`
52
+ }
53
+ return progress.bad_test_bounces > 0
54
+ ? `test: replace a bad test for #${ticket.number} ${ticket.title}`
55
+ : `test: add failing tests for #${ticket.number} ${ticket.title}`
56
+ }
57
+
58
+ export const commitStep = ({
59
+ stage,
60
+ ticket,
61
+ progress,
62
+ }: {
63
+ stage: CommitStage
64
+ ticket: TicketSnapshot
65
+ progress: TicketProgress
66
+ }): BuildAction | null => {
67
+ if (progress.commits[stage] !== null) return null
68
+ const hits = progress.leftovers[stage]
69
+ if (hits !== null && hits.length > 0) {
70
+ return stuck({
71
+ ticket: ticket.number,
72
+ reason: 'leftovers_found',
73
+ detail: hits
74
+ .map(({ path, reason }) => `${path}: ${reason}`)
75
+ .join('\n'),
76
+ })
77
+ }
78
+ return {
79
+ type: 'commit_ticket',
80
+ ticket: ticket.number,
81
+ stage,
82
+ message: commitMessage({ stage, ticket, progress }),
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Whether a failed red check or gates get a lone rerun (#544) before any
88
+ * fix round: every old test it failed also failed in another ticket within
89
+ * 10 minutes (replay marks it `shared`), and this check has had none yet.
90
+ */
91
+ const loneRerunDue = ({
92
+ check,
93
+ progress,
94
+ which,
95
+ }: {
96
+ check: SharedFailureFields
97
+ progress: TicketProgress
98
+ which: LoneRerunCheck
99
+ }): boolean => check.shared === true && !progress.lone_reruns.includes(which)
100
+
101
+ /** The latest bad test, for a stuck detail (#490). */
102
+ export const badTestText = ({
103
+ progress,
104
+ }: {
105
+ progress: TicketProgress
106
+ }): string => badTestDetail({ bad_test: progress.bad_test })
107
+
108
+ /**
109
+ * The test-writer's half of a ticket: fresh tests, the red check and its fix
110
+ * loop, then the red commit. `null` once the red commit is made. A ticket
111
+ * whose work the test-writer found already on the base branch is marked
112
+ * done instead (#484); one that changes no behavior is stuck, as a refactor
113
+ * ticket without its label.
114
+ */
115
+ export const testStep = ({
116
+ snapshot,
117
+ ticket,
118
+ progress,
119
+ run_notes,
120
+ base_branch,
121
+ }: StepArgs & { base_branch: string }): BuildAction | null => {
122
+ const number = ticket.number
123
+ const { test_writer, red_check } = progress
124
+ if (test_writer === null) {
125
+ return launch({
126
+ role: 'test-writer',
127
+ snapshot,
128
+ ticket,
129
+ progress,
130
+ run_notes,
131
+ })
132
+ }
133
+ if (test_writer.outcome === 'already_done') {
134
+ // Its evidence is checked first (#495). A failed check is a failed
135
+ // try, which `failedTurnStep` takes before this step; checking the
136
+ // same evidence again would only loop, so one left here is stuck.
137
+ const check = progress.already_done_check
138
+ if (check === null) {
139
+ return checkAlreadyDoneStep({
140
+ ticket,
141
+ base_sha: progress.worktree?.base_sha ?? 'HEAD',
142
+ result: test_writer,
143
+ })
144
+ }
145
+ if (!check.ok) {
146
+ return stuck({
147
+ ticket: number,
148
+ reason: 'agent_failed',
149
+ detail: `The test-writer's "already_done" evidence did not check out:\n${check.problems.map((problem) => `- ${problem}`).join('\n')}`,
150
+ })
151
+ }
152
+ return alreadyDoneStep({
153
+ spec_number: snapshot.spec.number,
154
+ base_branch,
155
+ ticket,
156
+ result: test_writer,
157
+ })
158
+ }
159
+ if (test_writer.outcome === 'nothing_new_to_test') {
160
+ return stuck({
161
+ ticket: number,
162
+ reason: 'nothing_new_to_test',
163
+ detail:
164
+ `The test-writer says this ticket changes no behavior, so no new test can fail first: ${test_writer.summary || 'no reason given'}\n` +
165
+ `A ticket like that is a refactor ticket: add the \`${REFACTOR_LABEL}\` label and retry it.`,
166
+ })
167
+ }
168
+ const redCheck: Extract<BuildAction, { type: 'run_red_check' }> = {
169
+ type: 'run_red_check',
170
+ ticket: number,
171
+ criteria_ids: ticket.criteria.map(({ id }) => id),
172
+ mapping: test_writer.criteria,
173
+ }
174
+ // Its old tests failed in another ticket too: run it alone first, even
175
+ // after an `environment` rerun, before that can make it stuck.
176
+ if (
177
+ red_check !== null &&
178
+ !red_check.ok &&
179
+ loneRerunDue({ check: red_check, progress, which: 'red_check' })
180
+ ) {
181
+ return { ...redCheck, exclusive: true }
182
+ }
183
+ const blamed = environmentFailedDetail({
184
+ environment:
185
+ progress.environment?.check === 'red_check'
186
+ ? progress.environment
187
+ : null,
188
+ reruns: progress.environment_reruns,
189
+ latest: red_check,
190
+ })
191
+ if (blamed !== null) {
192
+ return stuck({
193
+ ticket: number,
194
+ reason: 'environment_failed',
195
+ detail: blamed,
196
+ })
197
+ }
198
+ if (red_check === null) return redCheck
199
+ if (!red_check.ok) {
200
+ const problems = red_check.problems.join('\n')
201
+ if (progress.red_fix_rounds >= MAX_FIX_ROUNDS) {
202
+ return stuck({
203
+ ticket: number,
204
+ reason: 'red_check_failed',
205
+ detail: `The red check still fails after ${MAX_FIX_ROUNDS} fix rounds:\n${problems}`,
206
+ })
207
+ }
208
+ const session_id = progress.sessions['test-writer']
209
+ const message = redFixMessage({ red_check })
210
+ if (session_id === undefined) {
211
+ return launch({
212
+ role: 'test-writer',
213
+ snapshot,
214
+ ticket,
215
+ progress,
216
+ run_notes,
217
+ follow_up: message,
218
+ })
219
+ }
220
+ return {
221
+ type: 'follow_up_agent',
222
+ ticket: number,
223
+ role: 'test-writer',
224
+ session_id,
225
+ message,
226
+ }
227
+ }
228
+ return commitStep({ stage: 'red', ticket, progress })
229
+ }
230
+
231
+ /**
232
+ * The implementer's half of a ticket: the code, the gates and their fix
233
+ * loop, then the green commit. `null` once the green commit is made.
234
+ */
235
+ export const codeStep = ({
236
+ snapshot,
237
+ ticket,
238
+ progress,
239
+ run_notes,
240
+ }: StepArgs): BuildAction | null => {
241
+ const number = ticket.number
242
+ const { implementer } = progress
243
+ if (implementer === null) {
244
+ return launch({
245
+ role: 'implementer',
246
+ snapshot,
247
+ ticket,
248
+ progress,
249
+ run_notes,
250
+ })
251
+ }
252
+ if (implementer.outcome === 'bad_test') {
253
+ if (isRefactorTicket({ ticket })) {
254
+ return stuck({
255
+ ticket: number,
256
+ reason: 'bad_test',
257
+ detail: `The implementer of a refactor ticket sent a test back as bad; there is no test-writer to fix it: ${badTestText({ progress })}`,
258
+ })
259
+ }
260
+ if (progress.bad_test_bounces > MAX_BAD_TEST_BOUNCES) {
261
+ return stuck({
262
+ ticket: number,
263
+ reason: 'bad_test',
264
+ detail: `${badTestTwiceText({ earlier: progress.bad_test_sources.at(-2) ?? 'implementer', latest: 'implementer' })}: ${badTestText({ progress })}`,
265
+ })
266
+ }
267
+ // Throw away the implementer's work; replay then clears the ticket's
268
+ // tests and code, so a fresh test-writer replaces the bad test.
269
+ return { type: 'reset_ticket_worktree', ticket: number }
270
+ }
271
+ return (
272
+ gateStep({ snapshot, ticket, progress, run_notes }) ??
273
+ emptyTestStep({ ticket, progress }) ??
274
+ commitStep({ stage: 'green', ticket, progress })
275
+ )
276
+ }
277
+
278
+ /** Why the engine sends back a mapped test that checks nothing (#581). */
279
+ export const EMPTY_TEST_REASON =
280
+ 'It passes but runs no expect(), so it checks nothing (#581).'
281
+
282
+ /**
283
+ * After a ticket's gates pass: a mapped test that passed with no `expect()`
284
+ * goes back to a fresh test-writer, like a bad test (#581), and counts toward
285
+ * the same cap. Not on a refactor ticket (it has no map), and not after a
286
+ * rebase (the code is already reviewed). An older journal's cases carry no
287
+ * count, so nothing is sent back. `null`: go on.
288
+ *
289
+ * It is not part of `gateStep`, which the review's fixes also use: their
290
+ * tests are not red-checked.
291
+ */
292
+ const emptyTestStep = ({
293
+ ticket,
294
+ progress,
295
+ }: {
296
+ ticket: TicketSnapshot
297
+ progress: TicketProgress
298
+ }): BuildAction | null => {
299
+ if (isRefactorTicket({ ticket }) || progress.rejoin !== null) return null
300
+ const cases = progress.gates?.cases ?? []
301
+ const empty = progress.red_tests.find((test) => {
302
+ const found = findCase({ cases, test })
303
+ return found?.status === 'passed' && found.assertions === 0
304
+ })
305
+ if (empty === undefined) return null
306
+ if (progress.bad_test_bounces >= MAX_BAD_TEST_BOUNCES) {
307
+ return stuck({
308
+ ticket: ticket.number,
309
+ reason: 'bad_test',
310
+ detail: `${badTestTwiceText({ earlier: progress.bad_test_sources.at(-1) ?? 'implementer', latest: 'engine' })}: "${empty.name}" in ${empty.file} runs no expect().`,
311
+ })
312
+ }
313
+ return {
314
+ type: 'reset_ticket_worktree',
315
+ ticket: ticket.number,
316
+ bad_test: {
317
+ file: empty.file,
318
+ name: empty.name,
319
+ reason: EMPTY_TEST_REASON,
320
+ },
321
+ }
322
+ }
323
+
324
+ /**
325
+ * The ticket's gates and their fix loop: failed gates go back to the same
326
+ * implementer session (a fresh one with the same message if it is gone),
327
+ * up to `MAX_FIX_ROUNDS` follow-ups. An `environment` answer reruns them
328
+ * without a fix round; a rerun that fails the same way, or one past
329
+ * `MAX_ENVIRONMENT_RERUNS`, is stuck (#613). `null` once the gates pass.
330
+ */
331
+ export const gateStep = ({
332
+ snapshot,
333
+ ticket,
334
+ progress,
335
+ run_notes,
336
+ }: StepArgs): BuildAction | null => {
337
+ const number = ticket.number
338
+ const { gates } = progress
339
+ // Its old tests failed in another ticket too: run them alone first, even
340
+ // after an `environment` rerun, before that can make it stuck.
341
+ if (
342
+ gates !== null &&
343
+ !gates.ok &&
344
+ loneRerunDue({ check: gates, progress, which: 'gates' })
345
+ ) {
346
+ return {
347
+ type: 'run_gates',
348
+ ticket: number,
349
+ target: 'ticket',
350
+ exclusive: true,
351
+ }
352
+ }
353
+ const blamed = environmentFailedDetail({
354
+ environment:
355
+ progress.environment?.check === 'gates'
356
+ ? progress.environment
357
+ : null,
358
+ reruns: progress.environment_reruns,
359
+ latest: gates,
360
+ })
361
+ if (blamed !== null) {
362
+ return stuck({
363
+ ticket: number,
364
+ reason: 'environment_failed',
365
+ detail: blamed,
366
+ })
367
+ }
368
+ if (gates === null) {
369
+ return { type: 'run_gates', ticket: number, target: 'ticket' }
370
+ }
371
+ if (gates.ok) return null
372
+ if (progress.gate_fix_rounds >= MAX_FIX_ROUNDS) {
373
+ return stuck({
374
+ ticket: number,
375
+ reason: 'gates_failed',
376
+ detail: `The gates still fail after ${MAX_FIX_ROUNDS} fix rounds:\n${failedChecks({ gates })}`,
377
+ })
378
+ }
379
+ const session_id = progress.sessions.implementer
380
+ const message = gateFixMessage({ gates })
381
+ if (session_id === undefined) {
382
+ return launch({
383
+ role: 'implementer',
384
+ snapshot,
385
+ ticket,
386
+ progress,
387
+ run_notes,
388
+ follow_up: message,
389
+ })
390
+ }
391
+ return {
392
+ type: 'follow_up_agent',
393
+ ticket: number,
394
+ role: 'implementer',
395
+ session_id,
396
+ message,
397
+ }
398
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * A ticket's join and rebase steps: joining the run branch, and fixing the
3
+ * ticket on top of it after a clash or failed gates, with its test updates.
4
+ */
5
+
6
+ import {
7
+ isRefactorTicket,
8
+ stuck,
9
+ type BuildAction,
10
+ type StepArgs,
11
+ } from './build-action'
12
+ import { launch } from './build-launch'
13
+ import { badTestText } from './decide-red-green'
14
+ import {
15
+ clashFixMessage,
16
+ failedChecks,
17
+ testsUpdatedMessage,
18
+ } from './fix-loop-text'
19
+ import { MAX_REJOINS, MAX_TEST_UPDATES } from './loop-caps'
20
+ import { badTestDetail } from './stuck-text'
21
+
22
+ import type { TicketSnapshot } from '../intake/intake-schemas'
23
+ import type { ReplayedWorktree, TicketProgress } from '../journal/replay'
24
+
25
+ /**
26
+ * The fix on top of the run branch after a rebase: first a fresh test-writer
27
+ * for clashed tests, then the implementer for clashed code. `null` once both
28
+ * have answered; the gates, the green commit, and a fresh review of the new
29
+ * changes follow as usual.
30
+ *
31
+ * A bad test here can't reset the worktree, which would throw the ticket's
32
+ * uncommitted change away. Instead its tests go back to the test-writer
33
+ * (`send_tests_back`, #489), up to `MAX_TEST_UPDATES` times per ticket, then
34
+ * it is stuck. A refactor ticket has no test-writer, so it is stuck at once.
35
+ */
36
+ export const rejoinStep = ({
37
+ snapshot,
38
+ ticket,
39
+ progress,
40
+ run_notes,
41
+ }: StepArgs): BuildAction | null => {
42
+ const { rejoin } = progress
43
+ if (rejoin === null) return null
44
+ if (progress.test_update !== null) {
45
+ return testUpdateStep({ snapshot, ticket, progress, run_notes })
46
+ }
47
+ if (progress.implementer?.outcome === 'bad_test') {
48
+ return sendTestsBackStep({ ticket, progress, rejoin })
49
+ }
50
+ if (rejoin.tests_pending) {
51
+ return launch({
52
+ role: 'test-writer',
53
+ snapshot,
54
+ ticket,
55
+ progress,
56
+ run_notes,
57
+ })
58
+ }
59
+ if (rejoin.code_pending) {
60
+ const session_id = progress.sessions.implementer
61
+ if (session_id === undefined) {
62
+ return launch({
63
+ role: 'implementer',
64
+ snapshot,
65
+ ticket,
66
+ progress,
67
+ run_notes,
68
+ })
69
+ }
70
+ return {
71
+ type: 'follow_up_agent',
72
+ ticket: ticket.number,
73
+ role: 'implementer',
74
+ session_id,
75
+ message: clashFixMessage({ rejoin }),
76
+ }
77
+ }
78
+ return null
79
+ }
80
+
81
+ /**
82
+ * A bad test after a rebase: its tests go back to the test-writer, or, past
83
+ * the cap or on a refactor ticket, it is stuck with the implementer's reason.
84
+ */
85
+ const sendTestsBackStep = ({
86
+ ticket,
87
+ progress,
88
+ rejoin,
89
+ }: {
90
+ ticket: TicketSnapshot
91
+ progress: TicketProgress
92
+ rejoin: NonNullable<TicketProgress['rejoin']>
93
+ }): BuildAction => {
94
+ const number = ticket.number
95
+ if (isRefactorTicket({ ticket })) {
96
+ return stuck({
97
+ ticket: number,
98
+ reason: 'bad_test',
99
+ detail: `The implementer of a refactor ticket sent a test back as bad while fixing the ticket on top of the run branch; there is no test-writer to update it: ${badTestText({ progress })}`,
100
+ })
101
+ }
102
+ if (progress.test_updates >= MAX_TEST_UPDATES) {
103
+ return stuck({
104
+ ticket: number,
105
+ reason: 'bad_test',
106
+ detail: `The implementer sent a test back as bad on top of the run branch after the test-writer updated the tests ${MAX_TEST_UPDATES} times: ${badTestText({ progress })}`,
107
+ })
108
+ }
109
+ return {
110
+ type: 'send_tests_back',
111
+ ticket: number,
112
+ round: progress.test_updates + 1,
113
+ bad_test: progress.bad_test ?? {
114
+ file: '',
115
+ name: '',
116
+ reason: progress.implementer?.summary ?? '',
117
+ },
118
+ from_sha: rejoin.from_sha,
119
+ base_sha: rejoin.base_sha,
120
+ }
121
+ }
122
+
123
+ /**
124
+ * The open test update after a rebase (#489): a fresh test-writer updates
125
+ * the tests (its prompt says what joined and why the test is wrong), then
126
+ * the implementer finishes the ticket (a follow-up in its session, or a
127
+ * fresh one). There is no red check here: the implementer's code is already
128
+ * in the worktree, so the updated tests may pass, and the red commit already
129
+ * proved the ticket's tests failed first. The test-writer's mapping must
130
+ * still give every criterion a test; then the gates run every test, the
131
+ * updated ones and the run branch's together, and a fresh reviewer checks
132
+ * the whole change.
133
+ */
134
+ const testUpdateStep = ({
135
+ snapshot,
136
+ ticket,
137
+ progress,
138
+ run_notes,
139
+ }: StepArgs): BuildAction => {
140
+ const update = progress.test_update
141
+ if (update === null || update.result === null) {
142
+ return launch({
143
+ role: 'test-writer',
144
+ snapshot,
145
+ ticket,
146
+ progress,
147
+ run_notes,
148
+ })
149
+ }
150
+ const { result } = update
151
+ if (result.outcome !== 'tests_written') {
152
+ return stuck({
153
+ ticket: ticket.number,
154
+ reason: 'bad_test',
155
+ detail:
156
+ `The implementer sent a test back as bad on top of the run branch, and the test-writer answered "${result.outcome}" instead of updating the tests: ${result.summary || 'no reason given'}\n` +
157
+ `The bad test: ${badTestDetail({ bad_test: update.bad_test })}`,
158
+ })
159
+ }
160
+ const unmapped = ticket.criteria
161
+ .map(({ id }) => id)
162
+ .filter(
163
+ (id) =>
164
+ !result.criteria.some(
165
+ ({ criterion_id, tests }) =>
166
+ criterion_id === id && tests.length > 0
167
+ )
168
+ )
169
+ if (unmapped.length > 0) {
170
+ return stuck({
171
+ ticket: ticket.number,
172
+ reason: 'red_check_failed',
173
+ detail: `After updating the tests on top of the run branch, the test-writer mapped no test to ${unmapped.join(', ')}. Every criterion needs a test.`,
174
+ })
175
+ }
176
+ const session_id = progress.sessions.implementer
177
+ if (session_id === undefined) {
178
+ return launch({
179
+ role: 'implementer',
180
+ snapshot,
181
+ ticket,
182
+ progress,
183
+ run_notes,
184
+ })
185
+ }
186
+ return {
187
+ type: 'follow_up_agent',
188
+ ticket: ticket.number,
189
+ role: 'implementer',
190
+ session_id,
191
+ message: testsUpdatedMessage({
192
+ bad_test: update.bad_test,
193
+ summary: result.summary,
194
+ }),
195
+ }
196
+ }
197
+
198
+ /**
199
+ * The join section of an approved ticket, for the first ticket in the join
200
+ * queue only: join, the gates on the run branch, the push. A clash or failed
201
+ * gates sends the ticket back onto the run branch, up to `MAX_REJOINS`
202
+ * times; after that it is stuck. `null` once pushed.
203
+ */
204
+ export const joinStep = ({
205
+ ticket,
206
+ progress,
207
+ run_branch,
208
+ }: {
209
+ ticket: TicketSnapshot
210
+ progress: TicketProgress
211
+ run_branch: ReplayedWorktree
212
+ }): BuildAction | null => {
213
+ const number = ticket.number
214
+ const { joined, join_gates } = progress
215
+ if (joined === null) {
216
+ return { type: 'join_run_branch', ticket: number }
217
+ }
218
+ if (!joined.ok) {
219
+ if (progress.rejoins >= MAX_REJOINS) {
220
+ return stuck({
221
+ ticket: number,
222
+ reason: 'join_failed',
223
+ detail: `The ticket still clashes with the run branch after ${MAX_REJOINS} rebases:\n${joined.error}`,
224
+ })
225
+ }
226
+ return {
227
+ type: 'rebase_ticket',
228
+ ticket: number,
229
+ cause: 'clash',
230
+ undo_first_sha: null,
231
+ }
232
+ }
233
+ if (join_gates === null) {
234
+ return { type: 'run_gates', ticket: number, target: 'run_branch' }
235
+ }
236
+ if (!join_gates.ok) {
237
+ if (progress.rejoins >= MAX_REJOINS) {
238
+ return stuck({
239
+ ticket: number,
240
+ reason: 'join_gates_failed',
241
+ detail: `The gates still fail after joining, after ${MAX_REJOINS} rebases:\n${failedChecks({ gates: join_gates })}`,
242
+ })
243
+ }
244
+ return {
245
+ type: 'rebase_ticket',
246
+ ticket: number,
247
+ cause: 'join_gates',
248
+ undo_first_sha: joined.shas[0] ?? null,
249
+ }
250
+ }
251
+ if (progress.pushed === null) {
252
+ return {
253
+ type: 'push_run_branch',
254
+ ticket: number,
255
+ branch: run_branch.branch,
256
+ }
257
+ }
258
+ return null
259
+ }