@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,38 @@
1
+ /**
2
+ * The cap on a final review role's failed tries (`failureCap`): the stuck step
3
+ * once a failed turn used up the role's tries, or its engine failures in a row.
4
+ */
5
+ import { stuck, type FinalReviewAction } from './final-review-action'
6
+ import { MAX_ENGINE_FAILURES, MAX_FIX_ROUNDS } from './loop-caps'
7
+
8
+ import type { FinalReviewState, ReplayedAgentFailure } from '../journal/replay'
9
+
10
+ /**
11
+ * The stuck step when a failed turn used up its role's tries (or engine
12
+ * failures in a row), else `null`: the role gets another go.
13
+ */
14
+ export const failureCap = ({
15
+ review,
16
+ failed,
17
+ }: {
18
+ review: FinalReviewState
19
+ failed: ReplayedAgentFailure
20
+ }): FinalReviewAction | null => {
21
+ const { role, error, failure } = failed
22
+ if (failure === 'engine') {
23
+ const count = review.engine_failures[role] ?? 0
24
+ return count >= MAX_ENGINE_FAILURES
25
+ ? stuck({
26
+ reason: 'agent_failed',
27
+ detail: `The engine failed to run the ${role} ${count} times in a row: ${error}`,
28
+ })
29
+ : null
30
+ }
31
+ const tries = review.failed_tries[role] ?? 0
32
+ return tries >= MAX_FIX_ROUNDS
33
+ ? stuck({
34
+ reason: 'agent_failed',
35
+ detail: `The ${role} failed ${tries} tries in the final review; the last one: ${error}`,
36
+ })
37
+ : null
38
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The final review's decision step, as types: its actions
3
+ * (`FinalReviewAction`, `isFinalReviewAction`), where it stands
4
+ * (`FinalReviewDecision`), what it reads (`DecideArgs`), and the stuck step
5
+ * (`stuck`).
6
+ */
7
+ import type { FinalFixerRole } from './final-review-text'
8
+
9
+ import type { PromptRunNote } from '../agents/role-prompts'
10
+ import type { LensName, LensRole } from '../agents/role-results'
11
+ import type { StuckReason } from '../journal/journal-record'
12
+ import type {
13
+ FinalReviewState,
14
+ ReplayedGates,
15
+ ReplayedSnapshot,
16
+ ReplayedWorktree,
17
+ } from '../journal/replay'
18
+ import type { CrashCounts } from '../journal/step-records'
19
+
20
+ /** The final review's steps, once every ticket pushed and before the PR. */
21
+ export type FinalReviewAction =
22
+ /**
23
+ * Start a final review round: read the run branch's HEAD, the files
24
+ * since `from_sha`, and the config's rule files, then journal
25
+ * `final_review_started`. `fresh`: the fresh pass (#548).
26
+ */
27
+ | {
28
+ type: 'start_final_review'
29
+ round: number
30
+ from_sha: string
31
+ lenses: LensName[]
32
+ fresh?: boolean
33
+ }
34
+ /** Start a fresh, read-only lens reviewer on the run branch. */
35
+ | { type: 'launch_lens'; lens: LensName; role: LensRole; prompt: string }
36
+ /** Run the config's slow tests on the run branch (#563), once round 1's lenses are in. */
37
+ | { type: 'run_final_slow_tests' }
38
+ /** Every lens due this round approved. */
39
+ | { type: 'pass_final_review' }
40
+ /** A fix round on the lenses' findings starts. */
41
+ | { type: 'start_final_fix'; round: number }
42
+ /** Start a fresh fixer on the run branch's worktree. */
43
+ | {
44
+ type: 'launch_final_fixer'
45
+ role: FinalFixerRole
46
+ prompt: string
47
+ may_edit_tests: boolean
48
+ }
49
+ /** Send a follow-up to a fixer's open session (failed gates, a failed try). */
50
+ | {
51
+ type: 'follow_up_final_fixer'
52
+ role: FinalFixerRole
53
+ session_id: string
54
+ message: string
55
+ }
56
+ /** Run the engine config's gates on the run branch's worktree. */
57
+ | { type: 'run_final_gates' }
58
+ /** Scan for leftovers, then commit the fixes on the run branch. */
59
+ | { type: 'commit_final_fix'; round: number; message: string }
60
+ /** Push the run branch with the fixes to `origin`. */
61
+ | { type: 'push_final_fixes'; branch: string }
62
+ /** The engine can't safely pick the final review's next step by itself. */
63
+ | { type: 'mark_final_review_stuck'; reason: StuckReason; detail: string }
64
+
65
+ const FINAL_REVIEW_ACTION_TYPES: ReadonlySet<string> = new Set<
66
+ FinalReviewAction['type']
67
+ >([
68
+ 'start_final_review',
69
+ 'launch_lens',
70
+ 'run_final_slow_tests',
71
+ 'pass_final_review',
72
+ 'start_final_fix',
73
+ 'launch_final_fixer',
74
+ 'follow_up_final_fixer',
75
+ 'run_final_gates',
76
+ 'commit_final_fix',
77
+ 'push_final_fixes',
78
+ 'mark_final_review_stuck',
79
+ ])
80
+
81
+ /**
82
+ * Whether an action is one of the final review's.
83
+ *
84
+ * @example
85
+ * isFinalReviewAction({ type: 'run_final_gates' }) // true
86
+ */
87
+ export const isFinalReviewAction = <Action extends { type: string }>(
88
+ action: Action
89
+ ): action is Extract<Action, FinalReviewAction> =>
90
+ FINAL_REVIEW_ACTION_TYPES.has(action.type)
91
+
92
+ /**
93
+ * Where the final review stands for the build half of the decision step:
94
+ * still working (these actions can run now), passed or shipped (open the
95
+ * PR), or stuck (end the run until a reply).
96
+ */
97
+ export type FinalReviewDecision =
98
+ | { status: 'working'; actions: FinalReviewAction[] }
99
+ | { status: 'passed' }
100
+ | { status: 'shipped' }
101
+ | { status: 'stuck'; reason: StuckReason; detail: string }
102
+
103
+ /** What the final review's decision step reads from the run. */
104
+ export type DecideArgs = {
105
+ review: FinalReviewState
106
+ /** Crashes in a row per scheduler key; the final review's are its own. */
107
+ crashes: CrashCounts
108
+ snapshot: ReplayedSnapshot
109
+ run_branch: ReplayedWorktree
110
+ /** The latest gates on the run branch. */
111
+ run_branch_gates: ReplayedGates | null
112
+ /** The run notes a fresh agent gets (`newestRunNotes`). */
113
+ run_notes: PromptRunNote[]
114
+ /** The config's test setup files; none if left out. */
115
+ setup_files?: string[]
116
+ }
117
+
118
+ /** The step that marks the final review stuck, with its reason. */
119
+ export const stuck = ({
120
+ reason,
121
+ detail,
122
+ }: {
123
+ reason: StuckReason
124
+ detail: string
125
+ }): FinalReviewAction => ({ type: 'mark_final_review_stuck', reason, detail })
@@ -5,6 +5,7 @@ import {
5
5
  findingList,
6
6
  FIX_RULES,
7
7
  gateResults,
8
+ handoffSection,
8
9
  responseText,
9
10
  RULING_RULES,
10
11
  } from './review-text'
@@ -15,13 +16,14 @@ import {
15
16
  type PromptRunNote,
16
17
  } from '../agents/role-prompts'
17
18
  import { lensRole, type Finding, type LensName } from '../agents/role-results'
18
- import type {
19
- FinalFinding,
20
- FinalReviewFix,
21
- FinalReviewState,
22
- ReplayedGates,
23
- ReplayedSnapshot,
24
- RuleFile,
19
+ import {
20
+ handoffRole,
21
+ type FinalFinding,
22
+ type FinalReviewFix,
23
+ type FinalReviewState,
24
+ type ReplayedGates,
25
+ type ReplayedSnapshot,
26
+ type RuleFile,
25
27
  } from '../journal/replay'
26
28
 
27
29
  /**
@@ -191,7 +193,8 @@ const findingsSection = ({
191
193
  /**
192
194
  * The prompt a fresh final review fixer starts with: its task, the spec,
193
195
  * every ticket, and its findings (the test findings for the test-writer,
194
- * the code findings for the implementer). An implementer launched only to
196
+ * the code findings for the implementer, or the findings the other fixer
197
+ * answered `other_role`, #616). An implementer launched only to
195
198
  * fix failed gates (the round had only test findings) gets the gates'
196
199
  * output instead. The engine journals it word for word.
197
200
  *
@@ -224,7 +227,13 @@ export const finalFixerPrompt = ({
224
227
  "You are fixing the final review's findings on the whole run branch. The test fixes are in the worktree, uncommitted. The gates failed. Fix the code so every gate passes, then answer.",
225
228
  failedChecks({ gates }),
226
229
  ].join('\n\n')
227
- : findingsSection({ fix, kind })
230
+ : handoffRole({ fix }) === role
231
+ ? handoffSection({
232
+ fix,
233
+ role,
234
+ heading: `Final review fix round ${fix.round}`,
235
+ })
236
+ : findingsSection({ fix, kind })
228
237
  return [
229
238
  `# Your role: ${role}`,
230
239
  FIXER_TASKS[role],
@@ -248,31 +257,68 @@ export const openFinalFindingsText = ({
248
257
  findings: FinalFinding[]
249
258
  }): string => findingList(findings)
250
259
 
260
+ /** Each open finding as a PR line, with its lens, severity, and file. */
261
+ const openFindingLines = ({
262
+ findings,
263
+ }: {
264
+ findings: FinalFinding[]
265
+ }): string[] =>
266
+ findings.map(
267
+ ({ lens, severity, file, id, title }) =>
268
+ `- ${lens} lens, ${SEVERITY_TEXT[severity]}${file === null ? '' : ` (${file})`}: ${id} ${title}`
269
+ )
270
+
271
+ /**
272
+ * The "Open findings" section a PR starts with when the fresh pass's
273
+ * re-check (#548) still holds findings open: the final review passed
274
+ * without them, so the owner reviews them.
275
+ *
276
+ * @example
277
+ * freshFindingsSection({ findings })
278
+ * // '## Open findings\n\n...\n\n- security lens, blocker (src/sum.ts): security-S2 Sum overflows silently'
279
+ */
280
+ export const freshFindingsSection = ({
281
+ findings,
282
+ }: {
283
+ findings: FinalFinding[]
284
+ }): string =>
285
+ [
286
+ '## Open findings',
287
+ "After the final review's last fix round, every lens read the whole branch once more. Its findings got one fix round, and the lenses still hold these open, so this PR opened with them for you to review:",
288
+ openFindingLines({ findings }).join('\n'),
289
+ ].join('\n\n')
290
+
251
291
  /**
252
292
  * The "Open findings" section a shipped final review's PR starts with: why
253
- * it was stuck, and each finding still open with its lens, severity, and
254
- * file.
293
+ * it was stuck, each finding still open with its lens, severity, and file,
294
+ * and the files of the edits left uncommitted at ship time (#509), which
295
+ * went in their own commit without a review.
255
296
  *
256
297
  * @example
257
- * shippedFindingsSection({ findings, stuck })
298
+ * shippedFindingsSection({ findings, stuck, edits: null })
258
299
  * // '## Open findings\n\n...\n\n- security lens, should-fix (src/sum.ts): security-S1 Sum trusts its input'
259
300
  */
260
301
  export const shippedFindingsSection = ({
261
302
  findings,
262
303
  stuck,
304
+ edits,
263
305
  }: {
264
306
  findings: FinalFinding[]
265
307
  stuck: { reason: string; detail: string }
308
+ /** The commit of the edits left uncommitted at ship time, if any. */
309
+ edits: { message: string; files: string[] } | null
266
310
  }): string => {
267
- const lines = findings.map(
268
- ({ lens, severity, file, id, title }) =>
269
- `- ${lens} lens, ${SEVERITY_TEXT[severity]}${file === null ? '' : ` (${file})`}: ${id} ${title}`
270
- )
311
+ const lines = openFindingLines({ findings })
271
312
  return [
272
313
  '## Open findings',
273
314
  `The final review got stuck (\`${stuck.reason}\`) and you replied \`ship\`, so this PR opened with the final review unfinished.`,
274
315
  lines.length === 0
275
316
  ? `No lens finding was open. Why it was stuck:\n\n${stuck.detail}`
276
317
  : `These findings are still open:\n\n${lines.join('\n')}`,
277
- ].join('\n\n')
318
+ edits === null || edits.files.length === 0
319
+ ? ''
320
+ : `Uncommitted fixer edits, not reviewed: these files were changed in the run branch's worktree when you replied \`ship\`, so they went in their own commit, "${edits.message}":\n\n${edits.files.map((file) => `- ${file}`).join('\n')}`,
321
+ ]
322
+ .filter((part) => part !== '')
323
+ .join('\n\n')
278
324
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The comment on the spec issue when the final review is stuck.
3
+ */
4
+ import compact from 'lodash/compact'
5
+
6
+ import { fenced, plural } from './stuck-format'
7
+ import { leftoverAllowHint, reasonLine } from './ticket-stuck-text'
8
+
9
+ import type { StuckReason } from '../journal/journal-record'
10
+ import type { FinalReviewState, ReplayedWorktree } from '../journal/replay'
11
+
12
+ /** Why the final review is stuck, in one line, where it differs from a ticket. */
13
+ const FINAL_REASON_LINES: Partial<Record<StuckReason, string>> = {
14
+ changes_requested: 'The final review still asks for changes.',
15
+ gates_failed: "The checks still fail after the final review's fixes.",
16
+ bad_test:
17
+ "While fixing the final review's findings, the implementer sent a test back as bad.",
18
+ agent_failed: 'A final review agent failed on every try it had.',
19
+ crashed:
20
+ 'The engine crashed in the same final review step, again and again.',
21
+ environment_failed:
22
+ "A fixer says the final checks' failure is not the branch's fault, but the rerun failed the same way, or its reruns ran out.",
23
+ }
24
+
25
+ /** What the owner could do about the stuck final review, per reason. */
26
+ const finalSuggestion = ({ reason }: { reason: StuckReason }): string => {
27
+ switch (reason) {
28
+ case 'changes_requested':
29
+ return "Fix the open findings in the run branch's worktree, then reply `retry`; or reply `ship` if they can wait for review in the PR."
30
+ case 'leftovers_found':
31
+ return `Delete those files in the run branch's worktree, then reply \`retry\`. ${leftoverAllowHint({ retry: '`retry`' })}`
32
+ case 'setup_change_needed':
33
+ return "Make that change yourself in the run branch's worktree, then reply `retry`; or reply `ship` to open the PR anyway."
34
+ default:
35
+ return "Read the last error, fix what trips it in the run branch's worktree, then reply `retry`; or reply `ship` to open the PR anyway."
36
+ }
37
+ }
38
+
39
+ /**
40
+ * The comment on the spec issue when the final review is stuck: why (one
41
+ * line), what was tried, the last error, a suggestion, and the replies:
42
+ * `retry`, `stop`, and `ship` (never `skip`).
43
+ *
44
+ * @example
45
+ * finalStuckComment({ review, run_branch })
46
+ * // '**The final review is stuck**\n\nWhy: The final review still asks for changes. ...'
47
+ */
48
+ export const finalStuckComment = ({
49
+ review,
50
+ run_branch,
51
+ }: {
52
+ review: FinalReviewState
53
+ run_branch: ReplayedWorktree
54
+ }): string => {
55
+ const stuck = review.stuck ?? { reason: 'agent_failed', detail: '' }
56
+ const tries = Object.entries(review.failed_tries).flatMap(
57
+ ([role, count]) =>
58
+ count === undefined || count === 0
59
+ ? []
60
+ : [`${plural(count, 'failed try')} by the ${role}`]
61
+ )
62
+ const tried = compact([
63
+ review.round > 1 && plural(review.round - 1, 'fix round'),
64
+ review.gate_fix_rounds > 0 &&
65
+ `${plural(review.gate_fix_rounds, 'fix round')} on the checks`,
66
+ ...tries,
67
+ ])
68
+ return [
69
+ '**The final review is stuck**',
70
+ [
71
+ `Why: ${FINAL_REASON_LINES[stuck.reason] ?? reasonLine({ reason: stuck.reason })}`,
72
+ `Tried: ${tried.length === 0 ? 'Nothing more: another try would fail the same way.' : `${tried.join(', ')}.`}`,
73
+ `Suggestion: ${finalSuggestion({ reason: stuck.reason })}`,
74
+ `Run branch worktree: \`${run_branch.path}\` (a \`retry\` keeps your edits there)`,
75
+ ].join('\n'),
76
+ `Last error:\n\n${fenced(stuck.detail)}`,
77
+ [
78
+ 'Reply with one word on this issue:',
79
+ '- `retry`: fix the open findings again with fresh agents and fresh counts, keeping your edits.',
80
+ '- `ship`: open the PR anyway, with the open findings listed at the top.',
81
+ '- `stop`: end the run without a PR. The branch is kept.',
82
+ '',
83
+ 'Only the spec owner counts.',
84
+ ].join('\n'),
85
+ ].join('\n\n')
86
+ }
@@ -1,4 +1,5 @@
1
1
  import { rejoinOpening, type RejoinContext } from '../agents/role-prompts'
2
+ import type { BadTest } from '../agents/role-results'
2
3
  import type { AgentFailure } from '../journal/journal-record'
3
4
  import type { ReplayedGates, ReplayedRedCheck } from '../journal/replay'
4
5
  import { clipOutput } from '../shell/run-command'
@@ -49,13 +50,35 @@ const FAILED_TRY_OPENINGS: Record<Exclude<AgentFailure, 'engine'>, string> = {
49
50
  agent: 'Your last turn failed before it gave a result.',
50
51
  result: "Your last turn ended without a result that fits your role's schema.",
51
52
  guard: 'Your last turn changed things your role may not change.',
53
+ evidence:
54
+ 'The engine checked the evidence in your "already_done" answer, and some of it did not check out.',
55
+ chore: 'The engine could not do every chore your last result asked for ("file_modes", "run_generators").',
56
+ }
57
+
58
+ const FIX_CHORES =
59
+ 'Your work is still in the worktree, with every chore the engine could do. Fix what the error names (ask only for paths your role may write and generators the repo names, and fix what made a generator fail), then answer with your full result.'
60
+
61
+ const TRY_AGAIN =
62
+ "The engine undid every change your role may not make; the rest of your work is still in the worktree. Try again, keeping to your role's rules, then answer with your full result."
63
+
64
+ const CHECK_EVIDENCE_AGAIN =
65
+ 'If you named the wrong commits or tests, answer "already_done" again with the right ones: commits on the base branch, and tests that exist and pass there. ' +
66
+ 'If the work is not all on the base branch, or a criterion has no passing test there, write the tests and answer "tests_written" instead.'
67
+
68
+ /** What to do next, for a failure whose closing isn't `TRY_AGAIN`. */
69
+ const FAILED_TRY_CLOSINGS: Partial<Record<AgentFailure, string>> = {
70
+ evidence: CHECK_EVIDENCE_AGAIN,
71
+ chore: FIX_CHORES,
52
72
  }
53
73
 
54
74
  /**
55
75
  * The follow-up an agent gets after a failed try (its turn failed, gave no
56
76
  * usable result, or broke its role's rules): what failed, the error, that
57
77
  * the engine undid every change the role may not make, and to try again.
58
- * The engine journals it word for word.
78
+ * A test-writer whose `already_done` evidence did not check out (#495)
79
+ * hears what did not, and to fix its answer or write the tests instead.
80
+ * A writer whose chores the engine could not do (#618) hears to fix what
81
+ * the error names. The engine journals it word for word.
59
82
  *
60
83
  * @example
61
84
  * const message = failedTryMessage({ failure: 'guard', error: '- wrote src/sum.ts, which a test-writer may not write' })
@@ -70,7 +93,7 @@ export const failedTryMessage = ({
70
93
  [
71
94
  FAILED_TRY_OPENINGS[failure],
72
95
  `## Error\n\n${clipOutput({ text: error })}`,
73
- "The engine undid every change your role may not make; the rest of your work is still in the worktree. Try again, keeping to your role's rules, then answer with your full result.",
96
+ FAILED_TRY_CLOSINGS[failure] ?? TRY_AGAIN,
74
97
  ].join('\n\n')
75
98
 
76
99
  /**
@@ -94,6 +117,30 @@ export const clashFixMessage = ({
94
117
  'Never edit a test file. Make every gate pass, then answer again.',
95
118
  ].join('\n\n')
96
119
 
120
+ /**
121
+ * What the implementer is told once the test-writer updated the tests it
122
+ * sent back after a rebase (#489): which test, the test-writer's summary,
123
+ * and to finish the ticket on top of the run branch. A follow-up's message,
124
+ * or a fresh implementer's prompt section. The engine journals it word for
125
+ * word.
126
+ *
127
+ * @example
128
+ * const message = testsUpdatedMessage({ bad_test, summary: 'Expect the hint line.' })
129
+ */
130
+ export const testsUpdatedMessage = ({
131
+ bad_test,
132
+ summary,
133
+ }: {
134
+ bad_test: BadTest
135
+ summary: string
136
+ }): string =>
137
+ [
138
+ '## The test-writer updated the tests',
139
+ `You sent back ${[bad_test.file, bad_test.name].filter(Boolean).join(' > ') || 'a test'} as a bad test. The test-writer updated this ticket's tests to fit the run branch${summary.trim() === '' ? '.' : `: ${summary.trim()}`}`,
140
+ 'Finish the ticket on top of the run branch: resolve any conflict markers left, keep what the run branch has and what this ticket adds, and make every gate pass. ' +
141
+ 'Never edit a test file. If a test is still wrong, answer "bad_test" with your reason.',
142
+ ].join('\n\n')
143
+
97
144
  /**
98
145
  * The section a fresh agent gets when a crash cut off an earlier agent's
99
146
  * turn at its step: it starts fresh, and the worktree may hold that turn's
@@ -0,0 +1,120 @@
1
+ import type { LeftOutTicket } from '../intake/intake-schemas'
2
+ import { HUMAN_LABEL } from '../tracker/tracker'
3
+
4
+ /**
5
+ * Words for the tickets intake left out for a person (#499): a ticket with
6
+ * `ready-for-human`, or one that waits on such a ticket. The PR, the run's
7
+ * end, and the board all say it the same way.
8
+ */
9
+
10
+ /** Issue numbers as `#12`, `#12 and #13`, or `#12, #13 and #14`. */
11
+ const numbersText = ({ numbers }: { numbers: number[] }): string => {
12
+ const refs = numbers.map((number) => `#${number}`)
13
+ const last = refs.at(-1) ?? ''
14
+ return refs.length <= 1
15
+ ? last
16
+ : `${refs.slice(0, -1).join(', ')} and ${last}`
17
+ }
18
+
19
+ /**
20
+ * Why a left-out ticket isn't in the run, in words.
21
+ *
22
+ * @example
23
+ * leftOutWhy({ ticket: { reason: 'for_a_person', ... } }) // 'for a person'
24
+ * leftOutWhy({ ticket: { reason: 'waits_on_person', waits_on: [12], through: [13], ... } })
25
+ * // 'waits on #12, which is for a person (through #13)'
26
+ */
27
+ export const leftOutWhy = ({ ticket }: { ticket: LeftOutTicket }): string => {
28
+ if (ticket.reason === 'for_a_person') return 'for a person'
29
+ const which =
30
+ ticket.waits_on.length === 1
31
+ ? 'which is for a person'
32
+ : 'which are for a person'
33
+ const through =
34
+ ticket.through.length === 0
35
+ ? ''
36
+ : ` (through ${numbersText({ numbers: ticket.through })})`
37
+ return `waits on ${numbersText({ numbers: ticket.waits_on })}, ${which}${through}`
38
+ }
39
+
40
+ /**
41
+ * The PR's sections for the tickets left out for a person: "For a person"
42
+ * (the `ready-for-human` tickets) and "Waiting on a person" (the tickets
43
+ * blocked by them). Empty when none was left out.
44
+ *
45
+ * @example
46
+ * leftOutSections({ left_out })
47
+ * // ['## For a person\n\n...\n\n- #12 Make the store page', '## Waiting on a person\n\n...']
48
+ */
49
+ export const leftOutSections = ({
50
+ left_out,
51
+ }: {
52
+ left_out: LeftOutTicket[]
53
+ }): string[] => {
54
+ const people = left_out.filter(({ reason }) => reason === 'for_a_person')
55
+ const waiting = left_out.filter(
56
+ ({ reason }) => reason === 'waits_on_person'
57
+ )
58
+ return [
59
+ people.length === 0
60
+ ? ''
61
+ : `## For a person\n\nTickets labeled \`${HUMAN_LABEL}\`. An agent doesn't build them, so this PR doesn't close them.\n\n` +
62
+ people
63
+ .map(({ number, title }) => `- #${number} ${title}`)
64
+ .join('\n'),
65
+ waiting.length === 0
66
+ ? ''
67
+ : "## Waiting on a person\n\nTickets that can't be built until a person's ticket is done. A later run builds them.\n\n" +
68
+ waiting
69
+ .map(
70
+ (ticket) =>
71
+ `- #${ticket.number} ${ticket.title}: ${leftOutWhy({ ticket })}`
72
+ )
73
+ .join('\n'),
74
+ ].filter((section) => section !== '')
75
+ }
76
+
77
+ /**
78
+ * Why a run ended with nothing to do, in one line: every ticket closed,
79
+ * the work already done (#484), or the rest left for a person (#499).
80
+ *
81
+ * @example
82
+ * nothingToDoText({ content: { closed_tickets: [11], already_done: [], left_out: [] } })
83
+ * // 'Nothing to do: every ticket of the spec is closed.'
84
+ */
85
+ export const nothingToDoText = ({
86
+ content,
87
+ }: {
88
+ content: {
89
+ closed_tickets: number[]
90
+ already_done: number[]
91
+ left_out: LeftOutTicket[]
92
+ }
93
+ }): string => {
94
+ const numbersOf = (reason: LeftOutTicket['reason']) =>
95
+ content.left_out
96
+ .filter((ticket) => ticket.reason === reason)
97
+ .map(({ number }) => number)
98
+ const people = numbersOf('for_a_person')
99
+ const waiting = numbersOf('waits_on_person')
100
+ const parts = [
101
+ content.already_done.length === 0
102
+ ? ''
103
+ : `the work of ${content.already_done.map((number) => `#${number}`).join(', ')} was already on the base branch`,
104
+ people.length === 0
105
+ ? ''
106
+ : `${numbersText({ numbers: people })} ${people.length === 1 ? 'is' : 'are'} for a person`,
107
+ waiting.length === 0
108
+ ? ''
109
+ : `${numbersText({ numbers: waiting })} ${waiting.length === 1 ? 'waits' : 'wait'} on a ticket for a person`,
110
+ ].filter((part) => part !== '')
111
+ if (parts.length === 0) {
112
+ return 'Nothing to do: every ticket of the spec is closed.'
113
+ }
114
+ const last = parts.at(-1) ?? ''
115
+ const text =
116
+ parts.length === 1
117
+ ? last
118
+ : `${parts.slice(0, -1).join(', ')}, and ${last}`
119
+ return `Nothing to do: ${text}.`
120
+ }