@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,203 @@
1
+ /**
2
+ * The final review's fix round (`fixStep`): the fixers, the gates and their
3
+ * fix loop, the fix commit, and the push, after the lenses found blocking
4
+ * findings. Pure.
5
+ */
6
+ import { environmentFailedDetail } from './environment-rerun'
7
+ import { failureCap } from './final-failure-cap'
8
+ import {
9
+ stuck,
10
+ type DecideArgs,
11
+ type FinalReviewAction,
12
+ } from './final-review-action'
13
+ import {
14
+ finalFixerPrompt,
15
+ openFinalFindingsText,
16
+ type FinalFixerRole,
17
+ } from './final-review-text'
18
+ import {
19
+ CRASH_SECTION,
20
+ failedChecks,
21
+ failedTryMessage,
22
+ followUpSection,
23
+ gateFixMessage,
24
+ } from './fix-loop-text'
25
+ import { MAX_FIX_ROUNDS } from './loop-caps'
26
+ import { badTestDetail, setupChangeDetail } from './stuck-text'
27
+
28
+ import type { AgentRole } from '../agents/role-results'
29
+ import { handoffRole, type ReplayedAgentFailure } from '../journal/replay'
30
+
31
+ /**
32
+ * A fresh fixer of this role, told its findings (or the failed gates). With
33
+ * `follow_up`, it stands in for a follow-up whose session is gone, and its
34
+ * prompt carries the message. A fixer taking a turn a crash cut off is told
35
+ * of the crash.
36
+ */
37
+ const launchFixer = ({
38
+ role,
39
+ review,
40
+ snapshot,
41
+ run_notes,
42
+ follow_up,
43
+ }: DecideArgs & {
44
+ role: FinalFixerRole
45
+ follow_up?: string
46
+ }): FinalReviewAction => {
47
+ const { fix } = review
48
+ if (fix === null) throw new Error('No final review fix round is open.')
49
+ // An implementer with no code findings left is here for failed gates.
50
+ const gates =
51
+ role === 'implementer' && fix.code_answered ? review.gates : null
52
+ const sections = [
53
+ ...(follow_up === undefined
54
+ ? []
55
+ : [followUpSection({ message: follow_up })]),
56
+ ...(review.crashed_turn === role ? [CRASH_SECTION] : []),
57
+ ]
58
+ return {
59
+ type: 'launch_final_fixer',
60
+ role,
61
+ prompt: finalFixerPrompt({
62
+ role,
63
+ snapshot,
64
+ fix,
65
+ gates,
66
+ run_notes,
67
+ sections,
68
+ }),
69
+ may_edit_tests: role === 'test-writer',
70
+ }
71
+ }
72
+
73
+ const isFixer = (role: AgentRole): role is FinalFixerRole =>
74
+ role === 'test-writer' || role === 'implementer'
75
+
76
+ /**
77
+ * The next step after a fixer's failed turn: an engine failure starts a
78
+ * fresh fixer, any other a follow-up in its session (a fresh fixer with the
79
+ * same message with none), until its tries run out.
80
+ */
81
+ const failedFixerStep = ({
82
+ failed,
83
+ ...args
84
+ }: DecideArgs & { failed: ReplayedAgentFailure }): FinalReviewAction => {
85
+ const cap = failureCap({ review: args.review, failed })
86
+ if (cap !== null) return cap
87
+ const { role, error, failure, session_id } = failed
88
+ if (!isFixer(role)) throw new Error(`${role} is no final review fixer.`)
89
+ if (failure === 'engine') return launchFixer({ ...args, role })
90
+ const message = failedTryMessage({ failure, error })
91
+ if (session_id === null) {
92
+ return launchFixer({ ...args, role, follow_up: message })
93
+ }
94
+ return {
95
+ type: 'follow_up_final_fixer',
96
+ role,
97
+ session_id,
98
+ message,
99
+ }
100
+ }
101
+
102
+ /**
103
+ * A final review fix round, once every due lens finished and some finding
104
+ * blocks: start the round, a fresh test-writer for the test findings, then
105
+ * a fresh implementer for the code findings, then a fresh agent of the
106
+ * other writer role for findings a fixer answered `other_role` (#616), the
107
+ * gates (and their fix loop in the implementer's session, or a rerun when
108
+ * it answers `environment`, #613), the fix commit, and the push. `null` once
109
+ * the fixes are pushed.
110
+ */
111
+ export const fixStep = (args: DecideArgs): FinalReviewAction | null => {
112
+ const { review, run_branch, snapshot } = args
113
+ const { fix } = review
114
+ if (fix === null) return null
115
+ // The fresh pass's one fix round runs past the cap (#548).
116
+ if (fix.round > MAX_FIX_ROUNDS && !review.fresh) {
117
+ return stuck({
118
+ reason: 'changes_requested',
119
+ detail: `The final review still asks for changes after ${MAX_FIX_ROUNDS} fix rounds:\n${openFinalFindingsText({ findings: fix.findings })}`,
120
+ })
121
+ }
122
+ if (!review.fixing_started) {
123
+ return { type: 'start_final_fix', round: fix.round }
124
+ }
125
+ const failed = [
126
+ review.agent_failures['test-writer'],
127
+ review.agent_failures.implementer,
128
+ ].find((entry) => entry !== undefined)
129
+ if (failed !== undefined) return failedFixerStep({ ...args, failed })
130
+ if (review.setup_change !== null) {
131
+ return stuck({
132
+ reason: 'setup_change_needed',
133
+ detail: setupChangeDetail({
134
+ ...review.setup_change,
135
+ setup_files: args.setup_files ?? [],
136
+ }),
137
+ })
138
+ }
139
+ if (!fix.tests_answered)
140
+ return launchFixer({ ...args, role: 'test-writer' })
141
+ if (!fix.code_answered) return launchFixer({ ...args, role: 'implementer' })
142
+ if (fix.bad_test !== null) {
143
+ return stuck({
144
+ reason: 'bad_test',
145
+ detail: `While fixing the final review's findings, the implementer sent a test back as bad: ${badTestDetail({ bad_test: fix.bad_test })}`,
146
+ })
147
+ }
148
+ const handoff = handoffRole({ fix })
149
+ if (handoff !== null) return launchFixer({ ...args, role: handoff })
150
+ const { gates } = review
151
+ const blamed = environmentFailedDetail({
152
+ environment: review.environment,
153
+ reruns: review.environment_reruns,
154
+ latest: gates,
155
+ })
156
+ if (blamed !== null) {
157
+ return stuck({ reason: 'environment_failed', detail: blamed })
158
+ }
159
+ if (gates === null) return { type: 'run_final_gates' }
160
+ if (!gates.ok) {
161
+ if (review.gate_fix_rounds >= MAX_FIX_ROUNDS) {
162
+ return stuck({
163
+ reason: 'gates_failed',
164
+ detail: `The gates still fail after the final review's fixes, after ${MAX_FIX_ROUNDS} fix rounds:\n${failedChecks({ gates })}`,
165
+ })
166
+ }
167
+ const session_id = review.sessions.implementer
168
+ const message = gateFixMessage({ gates })
169
+ if (session_id === undefined) {
170
+ return launchFixer({
171
+ ...args,
172
+ role: 'implementer',
173
+ follow_up: message,
174
+ })
175
+ }
176
+ return {
177
+ type: 'follow_up_final_fixer',
178
+ role: 'implementer',
179
+ session_id,
180
+ message,
181
+ }
182
+ }
183
+ if (review.commit === null) {
184
+ const hits = review.leftovers
185
+ if (hits !== null && hits.length > 0) {
186
+ return stuck({
187
+ reason: 'leftovers_found',
188
+ detail: hits
189
+ .map(({ path, reason }) => `${path}: ${reason}`)
190
+ .join('\n'),
191
+ })
192
+ }
193
+ return {
194
+ type: 'commit_final_fix',
195
+ round: fix.round,
196
+ message: `fix: final review round ${fix.round} for spec #${snapshot.spec.number}`,
197
+ }
198
+ }
199
+ if (review.pushed === null) {
200
+ return { type: 'push_final_fixes', branch: run_branch.branch }
201
+ }
202
+ return null
203
+ }
@@ -1,136 +1,22 @@
1
1
  import { crashDetail, crashedOut } from './decide-crashes'
2
+ import { fixStep } from './decide-final-fix'
3
+ import { failureCap } from './final-failure-cap'
2
4
  import {
3
- finalFixerPrompt,
4
- lensPrompt,
5
- openFinalFindingsText,
6
- type FinalFixerRole,
7
- } from './final-review-text'
8
- import {
9
- CRASH_SECTION,
10
- failedChecks,
11
- failedTryMessage,
12
- followUpSection,
13
- gateFixMessage,
14
- } from './fix-loop-text'
15
- import { MAX_ENGINE_FAILURES, MAX_FIX_ROUNDS } from './loop-caps'
16
-
17
- import type { PromptRunNote } from '../agents/role-prompts'
18
- import {
19
- LENS_NAMES,
20
- lensRole,
21
- type AgentRole,
22
- type LensName,
23
- type LensRole,
24
- } from '../agents/role-results'
25
- import type { StuckReason } from '../journal/journal-record'
26
- import {
27
- isFinalKey,
28
- type FinalReviewState,
29
- type ReplayedAgentFailure,
30
- type ReplayedGates,
31
- type ReplayedSnapshot,
32
- type ReplayedWorktree,
33
- } from '../journal/replay'
34
- import type { CrashCounts } from '../journal/step-records'
35
-
36
- /** The final review's steps, once every ticket pushed and before the PR. */
37
- export type FinalReviewAction =
38
- /**
39
- * Start a final review round: read the run branch's HEAD, the files
40
- * since `from_sha`, and the config's rule files, then journal
41
- * `final_review_started`.
42
- */
43
- | {
44
- type: 'start_final_review'
45
- round: number
46
- from_sha: string
47
- lenses: LensName[]
48
- }
49
- /** Start a fresh, read-only lens reviewer on the run branch. */
50
- | { type: 'launch_lens'; lens: LensName; role: LensRole; prompt: string }
51
- /** Every lens due this round approved. */
52
- | { type: 'pass_final_review' }
53
- /** A fix round on the lenses' findings starts. */
54
- | { type: 'start_final_fix'; round: number }
55
- /** Start a fresh fixer on the run branch's worktree. */
56
- | {
57
- type: 'launch_final_fixer'
58
- role: FinalFixerRole
59
- prompt: string
60
- may_edit_tests: boolean
61
- }
62
- /** Send a follow-up to a fixer's open session (failed gates, a failed try). */
63
- | {
64
- type: 'follow_up_final_fixer'
65
- role: FinalFixerRole
66
- session_id: string
67
- message: string
68
- }
69
- /** Run the engine config's gates on the run branch's worktree. */
70
- | { type: 'run_final_gates' }
71
- /** Scan for leftovers, then commit the fixes on the run branch. */
72
- | { type: 'commit_final_fix'; round: number; message: string }
73
- /** Push the run branch with the fixes to `origin`. */
74
- | { type: 'push_final_fixes'; branch: string }
75
- /** The engine can't safely pick the final review's next step by itself. */
76
- | { type: 'mark_final_review_stuck'; reason: StuckReason; detail: string }
77
-
78
- const FINAL_REVIEW_ACTION_TYPES: ReadonlySet<string> = new Set<
79
- FinalReviewAction['type']
80
- >([
81
- 'start_final_review',
82
- 'launch_lens',
83
- 'pass_final_review',
84
- 'start_final_fix',
85
- 'launch_final_fixer',
86
- 'follow_up_final_fixer',
87
- 'run_final_gates',
88
- 'commit_final_fix',
89
- 'push_final_fixes',
90
- 'mark_final_review_stuck',
91
- ])
92
-
93
- /**
94
- * Whether an action is one of the final review's.
95
- *
96
- * @example
97
- * isFinalReviewAction({ type: 'run_final_gates' }) // true
98
- */
99
- export const isFinalReviewAction = <Action extends { type: string }>(
100
- action: Action
101
- ): action is Extract<Action, FinalReviewAction> =>
102
- FINAL_REVIEW_ACTION_TYPES.has(action.type)
5
+ stuck,
6
+ type DecideArgs,
7
+ type FinalReviewAction,
8
+ type FinalReviewDecision,
9
+ } from './final-review-action'
10
+ import { lensPrompt } from './final-review-text'
103
11
 
104
- /**
105
- * Where the final review stands for the build half of the decision step:
106
- * still working (these actions can run now), passed or shipped (open the
107
- * PR), or stuck (end the run until a reply).
108
- */
109
- export type FinalReviewDecision =
110
- | { status: 'working'; actions: FinalReviewAction[] }
111
- | { status: 'passed' }
112
- | { status: 'shipped' }
113
- | { status: 'stuck'; reason: StuckReason; detail: string }
12
+ import { LENS_NAMES, lensRole, type LensName } from '../agents/role-results'
13
+ import { isFinalKey } from '../journal/replay'
114
14
 
115
- type DecideArgs = {
116
- review: FinalReviewState
117
- /** Crashes in a row per scheduler key; the final review's are its own. */
118
- crashes: CrashCounts
119
- snapshot: ReplayedSnapshot
120
- run_branch: ReplayedWorktree
121
- /** The latest gates on the run branch. */
122
- run_branch_gates: ReplayedGates | null
123
- /** The run notes a fresh agent gets (`newestRunNotes`). */
124
- run_notes: PromptRunNote[]
125
- }
126
-
127
- const stuck = ({
128
- reason,
129
- detail,
130
- }: {
131
- reason: StuckReason
132
- detail: string
133
- }): FinalReviewAction => ({ type: 'mark_final_review_stuck', reason, detail })
15
+ export {
16
+ isFinalReviewAction,
17
+ type FinalReviewAction,
18
+ type FinalReviewDecision,
19
+ } from './final-review-action'
134
20
 
135
21
  /** A fresh lens, or the step after its failed try. */
136
22
  const lensStep = ({
@@ -158,185 +44,6 @@ const lensStep = ({
158
44
  return failureCap({ review, failed }) ?? launch
159
45
  }
160
46
 
161
- /**
162
- * The stuck step when a failed turn used up its role's tries (or engine
163
- * failures in a row), else `null`: the role gets another go.
164
- */
165
- const failureCap = ({
166
- review,
167
- failed,
168
- }: {
169
- review: FinalReviewState
170
- failed: ReplayedAgentFailure
171
- }): FinalReviewAction | null => {
172
- const { role, error, failure } = failed
173
- if (failure === 'engine') {
174
- const count = review.engine_failures[role] ?? 0
175
- return count >= MAX_ENGINE_FAILURES
176
- ? stuck({
177
- reason: 'agent_failed',
178
- detail: `The engine failed to run the ${role} ${count} times in a row: ${error}`,
179
- })
180
- : null
181
- }
182
- const tries = review.failed_tries[role] ?? 0
183
- return tries >= MAX_FIX_ROUNDS
184
- ? stuck({
185
- reason: 'agent_failed',
186
- detail: `The ${role} failed ${tries} tries in the final review; the last one: ${error}`,
187
- })
188
- : null
189
- }
190
-
191
- /**
192
- * A fresh fixer of this role, told its findings (or the failed gates). With
193
- * `follow_up`, it stands in for a follow-up whose session is gone, and its
194
- * prompt carries the message. A fixer taking a turn a crash cut off is told
195
- * of the crash.
196
- */
197
- const launchFixer = ({
198
- role,
199
- review,
200
- snapshot,
201
- run_notes,
202
- follow_up,
203
- }: DecideArgs & {
204
- role: FinalFixerRole
205
- follow_up?: string
206
- }): FinalReviewAction => {
207
- const { fix } = review
208
- if (fix === null) throw new Error('No final review fix round is open.')
209
- // An implementer with no code findings left is here for failed gates.
210
- const gates =
211
- role === 'implementer' && fix.code_answered ? review.gates : null
212
- const sections = [
213
- ...(follow_up === undefined
214
- ? []
215
- : [followUpSection({ message: follow_up })]),
216
- ...(review.crashed_turn === role ? [CRASH_SECTION] : []),
217
- ]
218
- return {
219
- type: 'launch_final_fixer',
220
- role,
221
- prompt: finalFixerPrompt({
222
- role,
223
- snapshot,
224
- fix,
225
- gates,
226
- run_notes,
227
- sections,
228
- }),
229
- may_edit_tests: role === 'test-writer',
230
- }
231
- }
232
-
233
- const isFixer = (role: AgentRole): role is FinalFixerRole =>
234
- role === 'test-writer' || role === 'implementer'
235
-
236
- /**
237
- * The next step after a fixer's failed turn: an engine failure starts a
238
- * fresh fixer, any other a follow-up in its session (a fresh fixer with the
239
- * same message with none), until its tries run out.
240
- */
241
- const failedFixerStep = ({
242
- failed,
243
- ...args
244
- }: DecideArgs & { failed: ReplayedAgentFailure }): FinalReviewAction => {
245
- const cap = failureCap({ review: args.review, failed })
246
- if (cap !== null) return cap
247
- const { role, error, failure, session_id } = failed
248
- if (!isFixer(role)) throw new Error(`${role} is no final review fixer.`)
249
- if (failure === 'engine') return launchFixer({ ...args, role })
250
- const message = failedTryMessage({ failure, error })
251
- if (session_id === null) {
252
- return launchFixer({ ...args, role, follow_up: message })
253
- }
254
- return {
255
- type: 'follow_up_final_fixer',
256
- role,
257
- session_id,
258
- message,
259
- }
260
- }
261
-
262
- /**
263
- * A final review fix round, once every due lens finished and some finding
264
- * blocks: start the round, a fresh test-writer for the test findings, then
265
- * a fresh implementer for the code findings, the gates (and their fix loop
266
- * in the implementer's session), the fix commit, and the push. `null` once
267
- * the fixes are pushed.
268
- */
269
- const fixStep = (args: DecideArgs): FinalReviewAction | null => {
270
- const { review, run_branch, snapshot } = args
271
- const { fix } = review
272
- if (fix === null) return null
273
- if (fix.round > MAX_FIX_ROUNDS) {
274
- return stuck({
275
- reason: 'changes_requested',
276
- detail: `The final review still asks for changes after ${MAX_FIX_ROUNDS} fix rounds:\n${openFinalFindingsText({ findings: fix.findings })}`,
277
- })
278
- }
279
- if (!review.fixing_started) {
280
- return { type: 'start_final_fix', round: fix.round }
281
- }
282
- const failed = [
283
- review.agent_failures['test-writer'],
284
- review.agent_failures.implementer,
285
- ].find((entry) => entry !== undefined)
286
- if (failed !== undefined) return failedFixerStep({ ...args, failed })
287
- if (!fix.tests_answered)
288
- return launchFixer({ ...args, role: 'test-writer' })
289
- if (!fix.code_answered) return launchFixer({ ...args, role: 'implementer' })
290
- if (fix.bad_test !== null) {
291
- const { file, name, reason } = fix.bad_test
292
- const where = [file, name].filter(Boolean).join(' > ')
293
- return stuck({
294
- reason: 'bad_test',
295
- detail: `While fixing the final review's findings, the implementer sent a test back as bad: ${where === '' ? reason : `${where}: ${reason}`}`,
296
- })
297
- }
298
- const { gates } = review
299
- if (gates === null) return { type: 'run_final_gates' }
300
- if (!gates.ok) {
301
- if (review.gate_fix_rounds >= MAX_FIX_ROUNDS) {
302
- return stuck({
303
- reason: 'gates_failed',
304
- detail: `The gates still fail after the final review's fixes, after ${MAX_FIX_ROUNDS} fix rounds:\n${failedChecks({ gates })}`,
305
- })
306
- }
307
- const session_id = review.sessions.implementer
308
- if (session_id === undefined) {
309
- return launchFixer({ ...args, role: 'implementer' })
310
- }
311
- return {
312
- type: 'follow_up_final_fixer',
313
- role: 'implementer',
314
- session_id,
315
- message: gateFixMessage({ gates }),
316
- }
317
- }
318
- if (review.commit === null) {
319
- const hits = review.leftovers
320
- if (hits !== null && hits.length > 0) {
321
- return stuck({
322
- reason: 'leftovers_found',
323
- detail: hits
324
- .map(({ path, reason }) => `${path}: ${reason}`)
325
- .join('\n'),
326
- })
327
- }
328
- return {
329
- type: 'commit_final_fix',
330
- round: fix.round,
331
- message: `fix: final review round ${fix.round} for spec #${snapshot.spec.number}`,
332
- }
333
- }
334
- if (review.pushed === null) {
335
- return { type: 'push_final_fixes', branch: run_branch.branch }
336
- }
337
- return null
338
- }
339
-
340
47
  /**
341
48
  * The final review: the decision step's part between the last ticket's push
342
49
  * and the PR. Pure. It always runs, even for a one-ticket spec.
@@ -350,8 +57,18 @@ const fixStep = (args: DecideArgs): FinalReviewAction | null => {
350
57
  * the run branch, and a push. Then only the lenses that had blocking
351
58
  * findings re-review only the new changes (from the last round's HEAD),
352
59
  * with their earlier findings and the fixers' answers, and rule on each
353
- * "won't fix". A review still asking for changes after `MAX_FIX_ROUNDS` fix
60
+ * "won't fix". With a slow test command (#563), round 1 also runs the slow
61
+ * tests once every lens finished, and settles only once they ran; failing
62
+ * ones open the fix round (or join it). A fix round with no finding, opened only
63
+ * by them, is just its gates (which run the slow tests again) with their
64
+ * fix loop, the commit, and the push, with no lens re-review.
65
+ * A review still asking for changes after `MAX_FIX_ROUNDS` fix
354
66
  * rounds is stuck, and so is a failed fix loop, a bad test, or a leftover.
67
+ * Once a fix commit was pushed and no blocking finding is left, the fresh
68
+ * pass (#548) has all five lenses read the whole branch again, without
69
+ * earlier findings; its blocking findings get one fix round (past the round
70
+ * cap, too) and one re-review, and what is still open then tops the PR
71
+ * instead of getting the review stuck.
355
72
  * Failed tries are capped per role like a ticket's. A stuck final review
356
73
  * waits for a reply; `ship` opens the PR anyway. A final review step cut
357
74
  * off by a crash `MAX_CRASHES` times in a row is stuck too.
@@ -399,9 +116,37 @@ export const decideFinalReview = (args: DecideArgs): FinalReviewDecision => {
399
116
  )
400
117
  return working(stop === undefined ? steps : [stop])
401
118
  }
402
- if (review.fix === null) return working([{ type: 'pass_final_review' }])
119
+ // Round 1's slow tests run once its lenses are in, never beside them:
120
+ // a lens's after-turn guard judges the run branch's worktree, so nothing
121
+ // else may write there while a lens runs. The round waits for them (#563).
122
+ if (review.slow_due && review.slow_tests === null) {
123
+ return working([{ type: 'run_final_slow_tests' }])
124
+ }
125
+ // Clean after a fix commit: every lens reads the whole branch once more,
126
+ // fresh, before the PR opens (#548). Once per final review; with no fix
127
+ // commit, round 1 was already a full fresh read.
128
+ const ending: FinalReviewAction =
129
+ review.fix_pushed && !review.fresh_started
130
+ ? {
131
+ type: 'start_final_review',
132
+ round: review.round + 1,
133
+ from_sha: run_branch.base_sha,
134
+ lenses: [...LENS_NAMES],
135
+ fresh: true,
136
+ }
137
+ : { type: 'pass_final_review' }
138
+ if (review.fix === null) return working([ending])
139
+ // The fresh pass's fix was re-checked: what is still open tops the PR,
140
+ // and the final review never gets stuck on it (#548).
141
+ if (review.fresh_started && !review.fresh) {
142
+ return working([{ type: 'pass_final_review' }])
143
+ }
403
144
  const step = fixStep(args)
404
145
  if (step !== null) return working([step])
146
+ // A fix round opened only by failing slow tests ends at its push: its
147
+ // gates ran the slow tests on what was pushed, and no lens had a finding
148
+ // to look at again (#563), so the fresh pass comes next.
149
+ if (review.fix.findings.length === 0) return working([ending])
405
150
  // The fixes are pushed: the lenses with blocking findings look again.
406
151
  return working([
407
152
  {