@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
@@ -3,10 +3,16 @@ import flatMap from 'lodash/flatMap'
3
3
  import uniq from 'lodash/uniq'
4
4
 
5
5
  import { alreadyDoneNote } from './already-done'
6
- import { shippedFindingsSection } from './final-review-text'
6
+ import { evidenceSection } from './evidence-text'
7
+ import {
8
+ freshFindingsSection,
9
+ shippedFindingsSection,
10
+ } from './final-review-text'
11
+ import { leftOutSections } from './left-out'
7
12
  import { newMemoriesSection } from './memory-text'
8
13
  import { reasonLine } from './stuck-text'
9
14
 
15
+ import type { AgentRole, MergeDanger } from '../agents/role-results'
10
16
  import {
11
17
  EMPTY_FINAL_REVIEW,
12
18
  type FinalReviewState,
@@ -24,15 +30,95 @@ const SEVERITY_TEXT = {
24
30
  const fileText = (file: string | null): string =>
25
31
  file === null ? '' : ` (${file})`
26
32
 
33
+ /**
34
+ * The longest Summary picture the PR body shows (#513); a longer one is
35
+ * clipped, so a runaway picture can't blow the body's budget.
36
+ */
37
+ export const MAX_SUMMARY_PICTURE = 3_000
38
+
39
+ const FENCE = '```'
40
+
41
+ /**
42
+ * The Summary section (#513): the integration lens's picture, clipped to
43
+ * `MAX_SUMMARY_PICTURE` (a fence the clip leaves open is closed), or a
44
+ * note that there is none.
45
+ */
46
+ const summarySection = (picture: string | null): string => {
47
+ if (picture === null) {
48
+ return '## Summary\n\nNot available: the final review gave no picture.'
49
+ }
50
+ if (picture.length <= MAX_SUMMARY_PICTURE) return `## Summary\n\n${picture}`
51
+ const clipped = picture.slice(0, MAX_SUMMARY_PICTURE)
52
+ const open = clipped.split(FENCE).length % 2 === 0
53
+ return `## Summary\n\n${clipped}${open ? `\n${FENCE}` : ''}\n\n… (clipped: the picture is over ${MAX_SUMMARY_PICTURE.toLocaleString('en-US')} characters)`
54
+ }
55
+
56
+ const DOOR_TEXT = { one_way: 'one-way', two_way: 'two-way' } as const
57
+
58
+ /** The Merge danger section (#513), or a note that there is none. */
59
+ const mergeDangerSection = (danger: MergeDanger | null): string =>
60
+ danger === null
61
+ ? '## Merge danger\n\nNot available: the final review gave no merge danger.'
62
+ : `## Merge danger\n\n- **Door:** ${DOOR_TEXT[danger.door]}: ${danger.door_reason}\n- **Blast radius:** ${danger.blast_radius}: ${danger.blast_radius_reason}`
63
+
64
+ /** One line per role that ran out of turns, under `where`. */
65
+ const outOfTurnsLines = ({
66
+ where,
67
+ counts,
68
+ }: {
69
+ where: string
70
+ counts: Partial<Record<AgentRole, number>>
71
+ }): string[] =>
72
+ Object.entries(counts).map(
73
+ ([role, times]) =>
74
+ `- ${where}: ${role}, ${times} ${times === 1 ? 'time' : 'times'}`
75
+ )
76
+
77
+ /**
78
+ * The Out of turns section (#543): each ticket's roles whose agents ran out
79
+ * of turns, and how often, then the final review's. Empty with none.
80
+ */
81
+ const outOfTurnsSection = ({
82
+ ticket_order,
83
+ tickets,
84
+ review,
85
+ }: {
86
+ ticket_order: number[]
87
+ tickets: Record<number, TicketProgress>
88
+ review: FinalReviewState
89
+ }): string => {
90
+ const lines = [
91
+ ...flatMap(ticket_order, (number) =>
92
+ outOfTurnsLines({
93
+ where: `#${number}`,
94
+ counts: tickets[number]?.out_of_turns ?? {},
95
+ })
96
+ ),
97
+ ...outOfTurnsLines({
98
+ where: 'the final review',
99
+ counts: review.out_of_turns,
100
+ }),
101
+ ]
102
+ return lines.length === 0
103
+ ? ''
104
+ : `## Out of turns\n\nThese agents ran out of turns: a sign the ticket may have been too big for one go.\n\n${lines.join('\n')}`
105
+ }
106
+
27
107
  /**
28
108
  * The run's pull request title and body, from the snapshot, each ticket's
29
109
  * progress, and the final review: which tickets it closes (an already-done
30
- * one with the commits that did its work, #484), the tickets left
110
+ * one with the commits that did its work, #484), the tickets left for a
111
+ * person and the ones waiting on them (#499), the tickets left
31
112
  * out (skipped, and why), the assumptions agents made (from every round of
32
- * every agent on each ticket, and in the final review), the reviews' nits,
113
+ * every agent on each ticket, and in the final review), the tickets whose
114
+ * agents ran out of turns (#543), the reviews' nits,
33
115
  * the findings declined through "won't fix", and the memories the learner
34
116
  * added or updated (#370). A final review shipped
35
- * while stuck puts its open findings at the very top.
117
+ * while stuck puts its open findings at the very top. Right after the
118
+ * "Built by" line come the Summary picture and the Merge Danger (from the
119
+ * final review's integration lens, or "Not available" without them) and
120
+ * the Evidence that each ticket's new tests failed first and pass now
121
+ * (#513), then the tickets.
36
122
  *
37
123
  * @example
38
124
  * const { title, body } = pullRequestText({ snapshot, tickets, final_review })
@@ -127,23 +213,36 @@ export const pullRequestText = ({
127
213
  })
128
214
  ),
129
215
  ]
216
+ const stillOpen = review.fix?.findings ?? []
130
217
  const shipped =
131
218
  review.shipped && review.stuck !== null
132
219
  ? shippedFindingsSection({
133
- findings: review.fix?.findings ?? [],
220
+ findings: stillOpen,
134
221
  stuck: review.stuck,
222
+ edits: review.shipped_edits,
135
223
  })
136
- : ''
224
+ : review.fresh_started && stillOpen.length > 0
225
+ ? freshFindingsSection({ findings: stillOpen })
226
+ : ''
137
227
  const body = [
138
228
  shipped,
139
229
  `Built by the Luca engine from spec #${spec.number}.`,
230
+ summarySection(review.summary_picture),
231
+ mergeDangerSection(review.merge_danger),
232
+ evidenceSection({ snapshot, tickets }),
140
233
  `## Tickets\n\n${closes.join('\n')}`,
234
+ ...leftOutSections({ left_out: snapshot.left_out }),
141
235
  skipped.length === 0
142
236
  ? ''
143
237
  : `## Skipped tickets\n\nLeft out of this run. They stay open for a later run.\n\n${skipped.join('\n')}`,
144
238
  assumptions.length + finalAssumptions.length === 0
145
239
  ? ''
146
240
  : `## Assumptions\n\nCalls agents made by themselves. Please check them.\n\n${[...assumptions, ...finalAssumptions].join('\n')}`,
241
+ outOfTurnsSection({
242
+ ticket_order: snapshot.ticket_order,
243
+ tickets,
244
+ review,
245
+ }),
147
246
  nits.length === 0
148
247
  ? ''
149
248
  : `## Nits\n\nSmall things the ticket reviews and the final review noted but did not send back.\n\n${nits.join('\n')}`,
@@ -158,6 +257,156 @@ export const pullRequestText = ({
158
257
  }
159
258
  }
160
259
 
260
+ /**
261
+ * What the engine keeps the PR body, and each of its PR comments, under
262
+ * (#508): a margin below GitHub's limit of 65,536 characters.
263
+ */
264
+ export const PULL_REQUEST_BUDGET = 60_000
265
+
266
+ /** The file in the run folder the PR's full text is saved to first (#508). */
267
+ export const PULL_REQUEST_FILE = 'pull-request.md'
268
+
269
+ /**
270
+ * The body's long parts, which move into PR comments when the body is over
271
+ * its budget. The rest (open findings, the Summary and Merge danger, the
272
+ * tickets it closes, and the ones left out) always stays in the body.
273
+ */
274
+ const MOVABLE_HEADINGS = [
275
+ '## Evidence',
276
+ '## Assumptions',
277
+ '## Out of turns',
278
+ '## Nits',
279
+ '## Declined findings',
280
+ '## New memories',
281
+ ]
282
+
283
+ const CLIPPED = `\n\n… (clipped: the full text is in the run folder's \`${PULL_REQUEST_FILE}\`)`
284
+
285
+ /** Room kept in each comment for its heading and the engine's marker. */
286
+ const COMMENT_ROOM = 200
287
+
288
+ /** `text`, clipped to stay under `limit` characters. */
289
+ const clip = ({ text, limit }: { text: string; limit: number }): string =>
290
+ text.length < limit
291
+ ? text
292
+ : `${text.slice(0, limit - CLIPPED.length - 1)}${CLIPPED}`
293
+
294
+ const isMovable = (section: string): boolean =>
295
+ MOVABLE_HEADINGS.some(
296
+ (heading) => section === heading || section.startsWith(`${heading}\n`)
297
+ )
298
+
299
+ /**
300
+ * A section cut into pieces under `limit`, between its list items; each
301
+ * piece after the first repeats its heading. An item over the limit alone
302
+ * is clipped.
303
+ */
304
+ const sectionPieces = ({
305
+ section,
306
+ limit,
307
+ }: {
308
+ section: string
309
+ limit: number
310
+ }): string[] => {
311
+ if (section.length < limit) return [section]
312
+ const heading = section.split('\n', 1)[0] ?? ''
313
+ const pieces: string[] = []
314
+ let current = ''
315
+ for (const item of section.split(/\n(?=- )/)) {
316
+ const joined = current === '' ? item : `${current}\n${item}`
317
+ if (joined.length < limit) {
318
+ current = joined
319
+ continue
320
+ }
321
+ if (current !== '') pieces.push(current)
322
+ current = clip({ text: `${heading} (continued)\n\n${item}`, limit })
323
+ }
324
+ return current === '' ? pieces : [...pieces, current]
325
+ }
326
+
327
+ /** Pieces packed into as few texts under `limit` as they fit in. */
328
+ const packed = ({
329
+ pieces,
330
+ limit,
331
+ }: {
332
+ pieces: string[]
333
+ limit: number
334
+ }): string[] =>
335
+ pieces.reduce<string[]>((texts, piece) => {
336
+ const last = texts.at(-1)
337
+ if (last !== undefined && `${last}\n\n${piece}`.length < limit) {
338
+ return [...texts.slice(0, -1), `${last}\n\n${piece}`]
339
+ }
340
+ return [...texts, piece]
341
+ }, [])
342
+
343
+ /**
344
+ * The PR body as sent to GitHub, and the PR comments posted after it, from
345
+ * its full text (#508). A body under `budget` goes as it is. A longer one
346
+ * keeps its essentials (open findings, the Summary and Merge danger, the
347
+ * tickets it closes, and the tickets left out), and its long parts (the
348
+ * Evidence, assumptions, out of turns, nits, declined findings, new
349
+ * memories) as long as
350
+ * they fit beside every essential (#513); the rest moves into PR
351
+ * comments, each under the budget, cut between list items, and the body
352
+ * says so. Whatever is still too long is clipped, pointing at the full
353
+ * text in the run folder.
354
+ *
355
+ * @example
356
+ * fitPullRequestBody({ body: full })
357
+ * // { body: '...## More in the PR comments\n\n...', comments: ['## Assumptions\n\n...'] }
358
+ */
359
+ export const fitPullRequestBody = ({
360
+ body,
361
+ budget = PULL_REQUEST_BUDGET,
362
+ }: {
363
+ body: string
364
+ budget?: number
365
+ }): { body: string; comments: string[] } => {
366
+ if (body.length < budget) return { body, comments: [] }
367
+ const note = (moved: string[]) =>
368
+ `## More in the PR comments\n\nThis PR's text is too long for its body, so its ${moved.map((section) => section.split('\n', 1)[0]?.replace('## ', '').toLowerCase()).join(', ')} are in the PR comments below. The full text is in the run folder's \`${PULL_REQUEST_FILE}\`.`
369
+ const sections = body.split(/\n\n(?=## )/)
370
+ // Essentials after a long part still need their room (#513).
371
+ const essentials = sections.filter((section) => !isMovable(section))
372
+ const kept: string[] = []
373
+ const kept_long: string[] = []
374
+ const moved: string[] = []
375
+ for (const section of sections) {
376
+ if (!isMovable(section)) {
377
+ kept.push(section)
378
+ continue
379
+ }
380
+ const fits =
381
+ moved.length === 0 &&
382
+ [...essentials, ...kept_long, section, note([section])].join('\n\n')
383
+ .length < budget
384
+ if (fits) {
385
+ kept.push(section)
386
+ kept_long.push(section)
387
+ } else moved.push(section)
388
+ }
389
+ const comments = packed({
390
+ pieces: moved.flatMap((section) =>
391
+ sectionPieces({ section, limit: budget - COMMENT_ROOM })
392
+ ),
393
+ limit: budget - COMMENT_ROOM,
394
+ })
395
+ return {
396
+ body: clip({
397
+ text: [...kept, ...(moved.length === 0 ? [] : [note(moved)])].join(
398
+ '\n\n'
399
+ ),
400
+ limit: budget,
401
+ }),
402
+ comments: comments.map((text, index) =>
403
+ comments.length === 1
404
+ ? text
405
+ : `**More of this PR's notes (${index + 1} of ${comments.length})**\n\n${text}`
406
+ ),
407
+ }
408
+ }
409
+
161
410
  /** One open ticket of the spec, as the tracker lists it. */
162
411
  type OpenTicket = { number: number; title: string }
163
412
 
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The texts of the repo wait (#615): the spec issue's comment when the run
3
+ * starts waiting for earlier runs on its repo, and when it is stuck on them.
4
+ */
5
+ import type { RepoWaitRun } from './decide-repo-wait'
6
+
7
+ /** The runs, as a list of lines. */
8
+ const runLines = (runs: RepoWaitRun[]): string =>
9
+ runs
10
+ .map(
11
+ ({ run_id, spec }) =>
12
+ `- run \`${run_id}\`${spec === null ? ' (a demo)' : ` (spec #${spec})`}`
13
+ )
14
+ .join('\n')
15
+
16
+ /**
17
+ * The comment on the spec issue when the run waits for earlier runs on its
18
+ * repo: which ones, and that it goes on by itself.
19
+ *
20
+ * @example
21
+ * repoWaitComment({ runs: [{ run_id: 'run-a', spec: 7 }] })
22
+ * // '**The run waits for another run on this repo**\n\n- run `run-a` (spec #7)\n\n...'
23
+ */
24
+ export const repoWaitComment = ({ runs }: { runs: RepoWaitRun[] }): string =>
25
+ [
26
+ '**The run waits for another run on this repo**',
27
+ runLines(runs),
28
+ 'Two runs on one repo slow each other down, so this one starts once none of these is going. It checks every minute and goes on by itself. After 24 hours it stops and asks you. To run two at once on purpose, start a run with `luca-run --alongside`.',
29
+ ].join('\n\n')
30
+
31
+ /**
32
+ * Why the run is stuck on its repo wait, in one line, naming the `ps`
33
+ * failure when the wait couldn't check whether those runs are going (#624).
34
+ *
35
+ * @example
36
+ * repoBusyDetail({ runs: [{ run_id: 'run-a', spec: 7 }], ps_error: null })
37
+ * // 'The run waited 24 hours for an earlier run on this repo: run-a (spec #7).'
38
+ */
39
+ export const repoBusyDetail = ({
40
+ runs,
41
+ ps_error = null,
42
+ }: {
43
+ runs: RepoWaitRun[]
44
+ ps_error?: string | null
45
+ }): string => {
46
+ const named = runs
47
+ .map(
48
+ ({ run_id, spec }) =>
49
+ `${run_id}${spec === null ? ' (a demo)' : ` (spec #${spec})`}`
50
+ )
51
+ .join(', ')
52
+ const ps =
53
+ ps_error === null
54
+ ? ''
55
+ : ` Luca couldn't list the processes (${ps_error}), so it couldn't tell whether those runs are still going.`
56
+ return `The run waited 24 hours for an earlier run on this repo: ${named}.${ps}`
57
+ }
58
+
59
+ /**
60
+ * The comment on the spec issue when the run is stuck after 24 hours of
61
+ * its repo wait (`repo_busy`): why, and the replies `retry` and `stop`.
62
+ *
63
+ * @example
64
+ * repoBusyStuckComment({ detail })
65
+ * // '**The run is stuck: another run on this repo is still going**\n\nWhy: ...'
66
+ */
67
+ export const repoBusyStuckComment = ({ detail }: { detail: string }): string =>
68
+ [
69
+ '**The run is stuck: another run on this repo is still going**',
70
+ [
71
+ `Why: ${detail}`,
72
+ 'Nothing has started yet: no ticket was read and no worktree made.',
73
+ ].join('\n'),
74
+ [
75
+ 'Reply with one word on this issue:',
76
+ '- `retry`: wait up to 24 hours more, then go on once no earlier run on the repo is going.',
77
+ '- `stop`: end the run. Start it again later, or with `luca-run --alongside` to run both at once.',
78
+ '',
79
+ 'Only the spec owner counts.',
80
+ ].join('\n'),
81
+ ].join('\n\n')
@@ -6,6 +6,7 @@ import type {
6
6
  ReplayedGates,
7
7
  ReviewFix,
8
8
  TicketProgress,
9
+ WriterRole,
9
10
  } from '../journal/replay'
10
11
  import { clipOutput } from '../shell/run-command'
11
12
 
@@ -59,6 +60,7 @@ export const responseText = ({
59
60
  }): string => {
60
61
  const answer = responses.find(({ finding_id }) => finding_id === finding.id)
61
62
  if (answer === undefined) return ' Fixer: no answer.'
63
+ // An `other_role` the other role never answered stands as "won't fix".
62
64
  return answer.response === 'fixed'
63
65
  ? ` Fixer: fixed. ${answer.reason}`.trimEnd()
64
66
  : ` Fixer: WON'T FIX. Reason: ${answer.reason || 'none given'}`
@@ -161,10 +163,62 @@ export const reviewSections = ({
161
163
 
162
164
  /** How a review fixer answers each finding it got. */
163
165
  export const FIX_RULES =
164
- 'For EACH finding, answer in "finding_responses": "fixed", or "wont_fix" with your reason if the finding is wrong. ' +
166
+ 'For EACH finding, answer in "finding_responses": "fixed", or "wont_fix" with your reason if the finding is wrong, ' +
167
+ 'or "other_role" with your reason if the other writer role must fix it, or must fix it too ' +
168
+ '(a code fix that needs a test, or a test that needs a non-test helper the test-writer may not write). ' +
169
+ 'Fix your part first; the engine then sends an "other_role" finding to a fresh agent of the other role, before the gates. ' +
165
170
  'A fresh reviewer rules on every "won\'t fix", so give a reason that stands on its own. ' +
166
171
  'The engine runs the gates again and then a fresh reviewer checks your changes.'
167
172
 
173
+ /** How the other role answers the findings handed to it (#616). */
174
+ const HANDOFF_RULES =
175
+ 'For EACH finding, answer in "finding_responses": "fixed", or "wont_fix" with your reason. ' +
176
+ 'Your answer is final: another "other_role" counts as "wont_fix", and a fresh reviewer rules on it. ' +
177
+ 'The engine runs the gates again and then a fresh reviewer checks the changes.'
178
+
179
+ const OTHER_WRITER: Record<WriterRole, WriterRole> = {
180
+ 'test-writer': 'implementer',
181
+ implementer: 'test-writer',
182
+ }
183
+
184
+ /**
185
+ * The findings the other writer role answered `other_role`, handed to
186
+ * `role` (#616), each with that fixer's reason, and how to answer them.
187
+ * `heading` names the fix round.
188
+ *
189
+ * @example
190
+ * const section = handoffSection({ fix, role: 'test-writer', heading: 'Review fix round 1' })
191
+ */
192
+ export const handoffSection = ({
193
+ fix,
194
+ role,
195
+ heading,
196
+ }: {
197
+ fix: Pick<ReviewFix, 'findings' | 'responses' | 'handed_off'>
198
+ role: WriterRole
199
+ heading: string
200
+ }): string => {
201
+ const ids = fix.handed_off[role] ?? []
202
+ const other = OTHER_WRITER[role]
203
+ const findings = fix.findings
204
+ .filter(({ id }) => ids.includes(id))
205
+ .map((finding) => {
206
+ const answer = fix.responses.find(
207
+ ({ finding_id, response }) =>
208
+ finding_id === finding.id && response === 'other_role'
209
+ )
210
+ return `${findingLine(finding)}\n The ${other} passed it on: ${answer?.reason || 'no reason given'}`
211
+ })
212
+ .join('\n')
213
+ return [
214
+ `## ${heading}: findings the ${other} passed to you`,
215
+ `The ${other} answered these findings "other_role": they need the ${role}'s work, instead of or as well as its own. ` +
216
+ `Its own fixes are in the worktree, uncommitted. Do your part for these findings only. Keep to your role's rules.`,
217
+ findings,
218
+ HANDOFF_RULES,
219
+ ].join('\n\n')
220
+ }
221
+
168
222
  /**
169
223
  * The findings a review fixer gets: the test findings for a fresh
170
224
  * test-writer, or the code findings for the implementer.
@@ -0,0 +1,16 @@
1
+ import type { JournalRecord } from '../journal/journal-record'
2
+
3
+ /**
4
+ * Helper shared by the crash-recovery end-to-end tests
5
+ * (`run-crash-recovery.e2e.test.ts` and
6
+ * `run-crash-recovery-git.e2e.test.ts`).
7
+ */
8
+
9
+ export const ofKind = <Kind extends JournalRecord['kind']>(
10
+ records: JournalRecord[],
11
+ kind: Kind
12
+ ): Extract<JournalRecord, { kind: Kind }>[] =>
13
+ records.filter(
14
+ (record): record is Extract<JournalRecord, { kind: Kind }> =>
15
+ record.kind === kind
16
+ )
@@ -0,0 +1,96 @@
1
+ import { expect } from 'bun:test'
2
+
3
+ import {
4
+ createScriptedLauncher,
5
+ type ScriptedLauncher,
6
+ type ScriptedTurn,
7
+ } from '../agents/scripted-launcher'
8
+ import type { JournalRecord } from '../journal/journal-record'
9
+ import {
10
+ CLEAN_LENS_TURNS,
11
+ EMPTY_LEARNER_TURN,
12
+ IMPLEMENTER_RESULT,
13
+ SUM_TEST,
14
+ } from '../testing/practice-repo'
15
+
16
+ /**
17
+ * Helpers shared by the end-to-end tests for closing finished agents'
18
+ * sessions (`run-session-close.e2e.test.ts`,
19
+ * `run-session-close-ticket-ends.e2e.test.ts`, and
20
+ * `run-session-close-interruptions.e2e.test.ts`).
21
+ */
22
+
23
+ /**
24
+ * A scripted launcher with these turns, then a clean final review of spec
25
+ * #10 and an empty learner to fall back on.
26
+ */
27
+ export const scriptedWith = (turns: ScriptedTurn[]): ScriptedLauncher =>
28
+ createScriptedLauncher({
29
+ turns: [...turns, ...CLEAN_LENS_TURNS(10), EMPTY_LEARNER_TURN(10)],
30
+ })
31
+
32
+ type SessionOf = { role: string | null; session_id: string }
33
+
34
+ export const byId = (a: SessionOf, b: SessionOf) =>
35
+ a.session_id.localeCompare(b.session_id)
36
+
37
+ /** Every session a scripted launcher started, with its agent's role. */
38
+ export const launchedBy = (launcher: ScriptedLauncher): SessionOf[] =>
39
+ launcher
40
+ .launches()
41
+ .filter(({ kind }) => kind === 'launch')
42
+ .map(({ role, session_id }) => ({ role, session_id }))
43
+
44
+ /** Every session close the journal records, with its agent's role. */
45
+ export const closedIn = (records: JournalRecord[]): SessionOf[] =>
46
+ records
47
+ .flatMap((record) =>
48
+ record.kind === 'agent_session_closed'
49
+ ? [{ role: record.role, session_id: record.content.session_id }]
50
+ : []
51
+ )
52
+ .toSorted(byId)
53
+
54
+ /** The journal's close of `session_id`, if any. */
55
+ export const closeOf = (records: JournalRecord[], session_id: string) =>
56
+ records.find(
57
+ (record) =>
58
+ record.kind === 'agent_session_closed' &&
59
+ record.content.session_id === session_id
60
+ )
61
+
62
+ /**
63
+ * No session the launcher started is still open, and the journal records
64
+ * each one's close exactly once.
65
+ */
66
+ export const expectEveryoneClosed = ({
67
+ launcher,
68
+ records,
69
+ }: {
70
+ launcher: ScriptedLauncher
71
+ records: JournalRecord[]
72
+ }) => {
73
+ expect(launcher.openSessions()).toEqual([])
74
+ expect(closedIn(records)).toEqual(launchedBy(launcher).toSorted(byId))
75
+ }
76
+
77
+ /** A ticket's launches and follow-ups, in order. */
78
+ export const callsOf = (launcher: ScriptedLauncher, ticket: number) =>
79
+ launcher.launches().filter((call) => call.ticket === ticket)
80
+
81
+ /** A sum test that defines sum itself, so it passes before any code exists. */
82
+ export const PASSING_SUM_TEST = SUM_TEST.replace(
83
+ "import { sum } from './sum'",
84
+ 'const sum = ({ numbers }: { numbers: number[] }) =>\n numbers.reduce((a, b) => a + b, 0)'
85
+ )
86
+
87
+ /** An implementer of `ticket` whose code fails the tests. */
88
+ export const wrongImplementer = (ticket: number): ScriptedTurn => ({
89
+ role: 'implementer',
90
+ ticket,
91
+ files: {
92
+ 'src/sum.ts': 'export const sum = (): number => 42\n',
93
+ 'src/index.ts': "export { sum } from './sum'\n",
94
+ },
95
+ result: IMPLEMENTER_RESULT,
96
+ })