@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,276 @@
1
+ /**
2
+ * The run branch's joins, undone and redone: a join a crash cut off
3
+ * (`openJoin`), undoing a ticket's join, rebasing a ticket onto the run
4
+ * branch, and sending its tests back to the test-writer after a rebase.
5
+ */
6
+ import uniq from 'lodash/uniq'
7
+
8
+ import { need, ticketWorktree, type BuildContext } from './build-context'
9
+ import type { BuildAction } from './decide-build'
10
+ import { installIn } from './execute-gates'
11
+ import { closeSessions, openSessionsIn } from './session-close'
12
+
13
+ import {
14
+ dependenciesChanged,
15
+ rebaseNeedsInstall,
16
+ } from '../gates/lockfile-install'
17
+ import { testFilesAmong } from '../gates/test-runner'
18
+ import type { JournalRecord } from '../journal/journal-record'
19
+ import type { RunState } from '../journal/replay'
20
+
21
+ /**
22
+ * Where the run branch stood before a join of `ticket` a crash cut off: the
23
+ * latest `join_started` for it with no `ticket_joined` or `ticket_stuck`
24
+ * for it after, else `null`.
25
+ *
26
+ * @example
27
+ * openJoin({ records: journal.read(), ticket: 11 }) // 'a1b2c3...' after a crash mid-join
28
+ */
29
+ export const openJoin = ({
30
+ records,
31
+ ticket,
32
+ }: {
33
+ records: JournalRecord[]
34
+ ticket: number
35
+ }): string | null => {
36
+ const last = records.findLast(
37
+ (record) =>
38
+ record.ticket === ticket &&
39
+ (record.kind === 'join_started' ||
40
+ record.kind === 'ticket_joined' ||
41
+ record.kind === 'ticket_stuck')
42
+ )
43
+ return last?.kind === 'join_started' ? last.content.run_branch_sha : null
44
+ }
45
+
46
+ /**
47
+ * Puts the run branch back where it stood before a join of `ticket` a crash
48
+ * cut off, dropping whatever half of its commits the crash left there. A
49
+ * cut-off step is taken again before other steps start, so nothing else
50
+ * moved the run branch since. Does nothing when no join was cut off.
51
+ */
52
+ export const undoCutOffJoin = async ({
53
+ context,
54
+ ticket,
55
+ }: {
56
+ context: BuildContext
57
+ ticket: number
58
+ }) => {
59
+ const sha = openJoin({ records: context.journal.read(), ticket })
60
+ if (sha === null) return
61
+ const runBranch = need({
62
+ value: context.state.run_branch,
63
+ what: 'run branch',
64
+ })
65
+ await context.git.resetWorktree({ cwd: runBranch.path, to: sha })
66
+ }
67
+
68
+ /**
69
+ * The run branch commits an undo of `ticket`'s join may take (#519): its
70
+ * own joined commits, and those of any other ticket that joined and has not
71
+ * pushed (a later join on top goes with it, and replay clears that join
72
+ * too). A pushed ticket's commits never.
73
+ */
74
+ const undoableCommits = ({
75
+ state,
76
+ ticket,
77
+ }: {
78
+ state: RunState
79
+ ticket: number
80
+ }): string[] =>
81
+ uniq(
82
+ Object.entries(state.tickets).flatMap(([number, progress]) => {
83
+ const { joined } = progress
84
+ if (!joined?.ok) return []
85
+ const own = Number(number) === ticket
86
+ return own || progress.pushed === null ? joined.shas : []
87
+ })
88
+ )
89
+
90
+ /**
91
+ * Undoes a ticket's join on the run branch, back to before `first_sha`, and
92
+ * returns the commits undone. It never rewinds over other work (#519): it
93
+ * may only undo the ticket's own join and later joins not pushed yet
94
+ * (`undoableCommits`). When git refuses, the run branch is left alone, the
95
+ * ticket is stuck (`undo_refused`) with why, and it returns `null`, so the
96
+ * caller does nothing more. The undone joins' install is still in the run
97
+ * branch's `node_modules`, so when their commits changed dependency files
98
+ * the lockfile's install runs there again.
99
+ */
100
+ export const undoJoin = async ({
101
+ context,
102
+ ticket,
103
+ first_sha,
104
+ }: {
105
+ context: BuildContext
106
+ ticket: number
107
+ first_sha: string
108
+ }): Promise<string[] | null> => {
109
+ const { git, state, journal, launcher } = context
110
+ const runBranch = need({ value: state.run_branch, what: 'run branch' })
111
+ const joined = state.tickets[ticket]?.joined
112
+ const tip = await git.head({ cwd: runBranch.path })
113
+ const undo = await git.undoReplay({
114
+ cwd: runBranch.path,
115
+ first_sha,
116
+ only: undoableCommits({ state, ticket }),
117
+ })
118
+ if (!undo.ok) {
119
+ // A stuck ticket's agents take no more follow-ups.
120
+ await closeSessions({
121
+ journal,
122
+ launcher,
123
+ sessions: openSessionsIn({ records: journal.read() }).filter(
124
+ (session) => session.ticket === ticket
125
+ ),
126
+ })
127
+ journal.append({
128
+ kind: 'ticket_stuck',
129
+ ticket,
130
+ role: null,
131
+ content: {
132
+ reason: 'undo_refused',
133
+ detail: `The engine went to undo this ticket's join on the run branch (from ${first_sha.slice(0, 7)}), but that would have rewound the run branch over other work, so the run branch was left alone:\n${undo.error}`,
134
+ },
135
+ })
136
+ return null
137
+ }
138
+ const { undone } = undo
139
+ const undoneFiles =
140
+ undone.length === 0
141
+ ? []
142
+ : await git.filesBetween({
143
+ cwd: runBranch.path,
144
+ from: `${first_sha}^`,
145
+ to: tip,
146
+ })
147
+ // A redo whose first try undid the join before a crash finds nothing
148
+ // left to undo: the commits it undid are the journaled join's.
149
+ const named =
150
+ undone.length === 0 && context.step.redo && joined?.ok === true
151
+ ? joined.shas
152
+ : undone
153
+ if (dependenciesChanged({ changed_files: undoneFiles })) {
154
+ await installIn({
155
+ journal,
156
+ cwd: runBranch.path,
157
+ target: 'run_branch',
158
+ ticket: null,
159
+ })
160
+ }
161
+ return named
162
+ }
163
+
164
+ /**
165
+ * Puts a joined ticket's change back on top of the run branch: undoes the
166
+ * join first if asked (its gates failed), then resets the ticket's worktree
167
+ * to the run branch's tip and applies the ticket's whole diff there,
168
+ * uncommitted, with conflict markers in the files that clash. Journals the
169
+ * clashed files split into tests and code. A refused undo (#519) leaves the
170
+ * ticket stuck, and nothing is rebased.
171
+ */
172
+ export const rebaseTicket = async ({
173
+ context,
174
+ action,
175
+ }: {
176
+ context: BuildContext
177
+ action: Extract<BuildAction, { type: 'rebase_ticket' }>
178
+ }) => {
179
+ const { git, state, journal, config } = context
180
+ const worktree = ticketWorktree({ state, ticket: action.ticket })
181
+ const runBranch = need({ value: state.run_branch, what: 'run branch' })
182
+ const progress = state.tickets[action.ticket]
183
+ // The ticket's latest commit: a review fix round's, or the green one.
184
+ const head = need({
185
+ value: progress?.commits.fix ?? progress?.commits.green,
186
+ what: `green commit for #${action.ticket}`,
187
+ })
188
+ const undone =
189
+ action.undo_first_sha === null
190
+ ? []
191
+ : await undoJoin({
192
+ context,
193
+ ticket: action.ticket,
194
+ first_sha: action.undo_first_sha,
195
+ })
196
+ // A refused undo left the run branch alone and the ticket stuck.
197
+ if (undone === null) return
198
+ const onto = await git.head({ cwd: runBranch.path })
199
+ const moved_files = await git.filesBetween({
200
+ cwd: worktree.path,
201
+ from: worktree.base_sha,
202
+ to: onto,
203
+ })
204
+ const ticket_files = await git.filesBetween({
205
+ cwd: worktree.path,
206
+ from: worktree.base_sha,
207
+ to: head,
208
+ })
209
+ const { conflicts } = await git.rebaseWorktree({
210
+ cwd: worktree.path,
211
+ from: worktree.base_sha,
212
+ to: head,
213
+ onto,
214
+ })
215
+ const tests = testFilesAmong({
216
+ files: conflicts,
217
+ test_file_patterns: config.test_file_patterns,
218
+ })
219
+ journal.append({
220
+ kind: 'ticket_rebased',
221
+ ticket: action.ticket,
222
+ role: null,
223
+ content: {
224
+ cause: action.cause,
225
+ base_sha: onto,
226
+ tests,
227
+ code: conflicts.filter((file) => !tests.includes(file)),
228
+ undone,
229
+ reinstall: rebaseNeedsInstall({ moved_files, ticket_files }),
230
+ },
231
+ })
232
+ }
233
+
234
+ /**
235
+ * Sends a ticket's tests back to its test-writer after a rebase (#489):
236
+ * looks up the tickets whose joined commits the run branch got between the
237
+ * ticket's old base and its new one, and the files the run branch changed
238
+ * there, then journals it all as `tests_sent_back`. Reads git only.
239
+ */
240
+ export const sendTestsBack = async ({
241
+ context,
242
+ action,
243
+ }: {
244
+ context: BuildContext
245
+ action: Extract<BuildAction, { type: 'send_tests_back' }>
246
+ }) => {
247
+ const { git, state, journal } = context
248
+ const { ticket, round, bad_test, from_sha, base_sha } = action
249
+ const worktree = ticketWorktree({ state, ticket })
250
+ const commits = await git.commitsBetween({
251
+ cwd: worktree.path,
252
+ from: from_sha,
253
+ to: base_sha,
254
+ })
255
+ const files = await git.filesBetween({
256
+ cwd: worktree.path,
257
+ from: from_sha,
258
+ to: base_sha,
259
+ })
260
+ const joined = Object.entries(state.tickets).flatMap(
261
+ ([number, progress]) => {
262
+ const other = Number(number)
263
+ const shas = progress.joined?.ok ? progress.joined.shas : []
264
+ if (other === ticket || !shas.some((sha) => commits.includes(sha)))
265
+ return []
266
+ const title = state.snapshot?.tickets[other]?.title ?? ''
267
+ return [{ ticket: other, title }]
268
+ }
269
+ )
270
+ journal.append({
271
+ kind: 'tests_sent_back',
272
+ ticket,
273
+ role: null,
274
+ content: { round, bad_test, from_sha, base_sha, joined, files },
275
+ })
276
+ }
@@ -1,6 +1,7 @@
1
1
  import type { MemoryAction, MemorySaveStep } from './decide-memory'
2
2
  import { buildContext, need, runTurn, type BuildDeps } from './execute-build'
3
3
 
4
+ import { launchPick } from '../jev/model-pick'
4
5
  import type { Journal } from '../journal/journal'
5
6
  import type { JournalRecord } from '../journal/journal-record'
6
7
  import { replayRun } from '../journal/replay'
@@ -407,6 +408,14 @@ export const executeMemoryAction = async ({
407
408
  value: built.state.spec_number,
408
409
  what: 'spec',
409
410
  })
411
+ const { model, model_reason } = launchPick({
412
+ records: journal.read(),
413
+ state: built.state,
414
+ config: built.config,
415
+ ticket: null,
416
+ role: 'learner',
417
+ step,
418
+ })
410
419
  journal.append({
411
420
  kind: 'agent_started',
412
421
  ticket: null,
@@ -415,6 +424,8 @@ export const executeMemoryAction = async ({
415
424
  role: 'learner',
416
425
  prompt: action.prompt,
417
426
  follow_up_of: null,
427
+ model,
428
+ model_reason,
418
429
  },
419
430
  })
420
431
  await runTurn({
@@ -433,6 +444,7 @@ export const executeMemoryAction = async ({
433
444
  config: built.config,
434
445
  // The learner writes nothing and talks to nobody.
435
446
  messaging: null,
447
+ model,
436
448
  }),
437
449
  })
438
450
  return
@@ -0,0 +1,84 @@
1
+ import { join } from 'node:path'
2
+
3
+ import type { BuildAction } from './decide-build'
4
+ import type { BuildContext } from './execute-build'
5
+ import { openTicketsNotInRun } from './not-in-run'
6
+ import {
7
+ fitPullRequestBody,
8
+ PULL_REQUEST_FILE,
9
+ withNotInRun,
10
+ } from './pull-request-text'
11
+
12
+ import { postCommentOnce } from '../tracker/post-comment-once'
13
+ import { isTrackerFailure, type OpenedPullRequest } from '../tracker/tracker'
14
+
15
+ const errorText = (error: unknown): string =>
16
+ error instanceof Error ? error.message : String(error)
17
+
18
+ /**
19
+ * Opens the run's one pull request, then journals `pull_request_opened`
20
+ * (#508). Its full text (with the spec's open tickets not in the run) is
21
+ * saved first to `pull-request.md` in the run folder, so it's never lost.
22
+ * An open PR from the run branch is reused, never opened twice: the one a
23
+ * try opened before a crash or a failure, or one the owner opened by hand.
24
+ * The body sent keeps under `PULL_REQUEST_BUDGET`, and what doesn't fit
25
+ * is posted as PR comments after it (`fitPullRequestBody`), each once. A
26
+ * failed tracker call (`isTrackerFailure`: finding or opening the PR, such
27
+ * as a failed `gh pr create`) journals the run stuck (`pull_request_failed`)
28
+ * with its error, for the owner's `retry`; any other error is a crash.
29
+ */
30
+ export const openPullRequest = async ({
31
+ context,
32
+ action,
33
+ }: {
34
+ context: BuildContext
35
+ action: Extract<BuildAction, { type: 'open_pull_request' }>
36
+ }): Promise<void> => {
37
+ const { journal, tracker, state, run_dir, step } = context
38
+ const { head, base, title, body } = action
39
+ // The spec's open tickets not in the run are named in the PR.
40
+ const full = withNotInRun({
41
+ body,
42
+ open: await openTicketsNotInRun({ tracker, state }),
43
+ })
44
+ await Bun.write(join(run_dir, PULL_REQUEST_FILE), full)
45
+ const fitted = fitPullRequestBody({ body: full })
46
+ let opened: OpenedPullRequest
47
+ try {
48
+ opened =
49
+ (await tracker.findOpenPullRequest({ head })) ??
50
+ (await tracker.openPullRequest({
51
+ head,
52
+ base,
53
+ title,
54
+ body: fitted.body,
55
+ }))
56
+ } catch (error) {
57
+ if (!isTrackerFailure(error)) throw error
58
+ journal.append({
59
+ kind: 'run_stuck',
60
+ ticket: null,
61
+ role: null,
62
+ content: {
63
+ reason: 'pull_request_failed',
64
+ detail: errorText(error),
65
+ },
66
+ })
67
+ return
68
+ }
69
+ for (const [n, comment] of fitted.comments.entries()) {
70
+ await postCommentOnce({
71
+ tracker,
72
+ number: opened.number,
73
+ body: comment,
74
+ step,
75
+ n,
76
+ })
77
+ }
78
+ journal.append({
79
+ kind: 'pull_request_opened',
80
+ ticket: null,
81
+ role: null,
82
+ content: { ...opened, head, base, title, body: full },
83
+ })
84
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * The `run_red_check` step: the test-writer's tests run in the ticket's
3
+ * worktree, checked against its baseline, its test map, and the feature
4
+ * map, and journaled as `red_check`.
5
+ */
6
+ import { join } from 'node:path'
7
+
8
+ import uniq from 'lodash/uniq'
9
+
10
+ import {
11
+ need,
12
+ reportFile,
13
+ testFilesIn,
14
+ ticketWorktree,
15
+ type BuildContext,
16
+ } from './build-context'
17
+ import type { BuildAction } from './decide-build'
18
+ import { leaveOutIn, ticketChanges, watchPrepare } from './worktree-changes'
19
+
20
+ import {
21
+ bunTestCommands,
22
+ featureMapPath,
23
+ hasSlowTests,
24
+ testTimeoutOf,
25
+ } from '../config/engine-config'
26
+ import { readFeatureMap } from '../gates/feature-map'
27
+ import { prepareCheck } from '../gates/gate-runner'
28
+ import { endedText } from '../gates/gate-schemas'
29
+ import { checkRed, type ChangedTestSource } from '../gates/red-check'
30
+ import {
31
+ fileTestsCommand,
32
+ runBunTests,
33
+ runTests,
34
+ testFilesAmong,
35
+ withOwnRun,
36
+ } from '../gates/test-runner'
37
+ import { pathText } from '../git/path-text'
38
+
39
+ /**
40
+ * The red check's problem when the test-writer changed no file (what
41
+ * prepare made aside): the red commit would have nothing to commit.
42
+ */
43
+ export const NO_RED_CHANGES =
44
+ 'No file changed, so the red commit would be empty: write the tests for the criteria in the worktree (new or changed test files).'
45
+
46
+ /**
47
+ * The repo's feature map as the red check needs it (#583), read from the
48
+ * worktree at `cwd`: its configured path (a file or a folder of pages,
49
+ * #597), and the test files it lists. `files` is `null` when the map is
50
+ * missing or can't be read, so the red check can name it rather than crash.
51
+ * `null` when the config names no map.
52
+ */
53
+ const featureMapIn = async ({
54
+ context,
55
+ cwd,
56
+ }: {
57
+ context: BuildContext
58
+ cwd: string
59
+ }): Promise<{ path: string; files: string[] | null } | null> => {
60
+ const path = featureMapPath({ config: context.config })
61
+ if (path === null) return null
62
+ return {
63
+ path,
64
+ files: await readFeatureMap({
65
+ cwd,
66
+ path,
67
+ test_file_patterns: context.config.test_file_patterns,
68
+ }),
69
+ }
70
+ }
71
+
72
+ export const runRedCheck = async ({
73
+ context,
74
+ action,
75
+ }: {
76
+ context: BuildContext
77
+ action: Extract<BuildAction, { type: 'run_red_check' }>
78
+ }) => {
79
+ const { path, base_sha } = ticketWorktree({
80
+ state: context.state,
81
+ ticket: action.ticket,
82
+ })
83
+ const baseline = need({
84
+ value: context.state.tickets[action.ticket]?.baseline,
85
+ what: `baseline test run for #${action.ticket}`,
86
+ })
87
+ const test_files = await testFilesIn({ context, cwd: path })
88
+ // Before prepare runs, so what it makes now can't count as a change.
89
+ const leave_out = await leaveOutIn({
90
+ context,
91
+ cwd: path,
92
+ ticket: action.ticket,
93
+ })
94
+ const changed = await ticketChanges({ context, cwd: path, leave_out })
95
+ // Every test file that differs from the ticket's base, not only from
96
+ // HEAD: after a bad-test bounce, HEAD may still hold an old red commit,
97
+ // whose test files the fresh test-writer may not touch (#581).
98
+ const base_changed = testFilesAmong({
99
+ files: (
100
+ await context.git.changedSince({ cwd: path, from: base_sha })
101
+ ).filter((file) => !leave_out.includes(file)),
102
+ test_file_patterns: context.config.test_file_patterns,
103
+ })
104
+ const prepare = await watchPrepare({
105
+ context,
106
+ cwd: path,
107
+ ticket: action.ticket,
108
+ run: () => prepareCheck({ config: context.config, cwd: path }),
109
+ })
110
+ const files = uniq(
111
+ action.mapping.flatMap(({ tests }) => tests.map(({ file }) => file))
112
+ )
113
+ // A lone rerun (#544) runs each test command alone.
114
+ const exclusive = action.exclusive === true
115
+ const fast = await runBunTests({
116
+ cwd: path,
117
+ commands: bunTestCommands(context),
118
+ test_files,
119
+ timeout_ms: testTimeoutOf({ config: context.config }),
120
+ test_concurrency: context.config.test_concurrency,
121
+ exclusive,
122
+ report_file: await reportFile({
123
+ context,
124
+ ticket: action.ticket,
125
+ label: 'red',
126
+ }),
127
+ })
128
+ // With a slow command, the fast tests leave out every slow test file
129
+ // (#564): the files the test map names or the ticket changed, that the
130
+ // fast run gave no results for, run on their own, with plain `bun test`.
131
+ // A run that gives no results at all leaves the fast run as it is (see
132
+ // `withOwnRun`): its files don't load, as the fast run said already.
133
+ const own = hasSlowTests({ config: context.config })
134
+ ? uniq([
135
+ ...files,
136
+ ...testFilesAmong({
137
+ files: changed.map((change) => change.path),
138
+ test_file_patterns: context.config.test_file_patterns,
139
+ }),
140
+ ]).filter((file) => fast.files_without_results.includes(file))
141
+ : []
142
+ const current =
143
+ own.length === 0
144
+ ? fast
145
+ : withOwnRun({
146
+ fast,
147
+ own: await runTests({
148
+ cwd: path,
149
+ command: fileTestsCommand({ files: own }),
150
+ test_files: own,
151
+ report_file: await reportFile({
152
+ context,
153
+ ticket: action.ticket,
154
+ label: 'red-own',
155
+ }),
156
+ timeout_ms: testTimeoutOf({ config: context.config }),
157
+ test_concurrency: context.config.test_concurrency,
158
+ exclusive,
159
+ }),
160
+ test_files,
161
+ })
162
+ const sources: Record<string, string | null> = {}
163
+ for (const file of files) {
164
+ sources[file] = await pathText({ path: join(path, file) })
165
+ }
166
+ // Each of those test files as it was at the ticket's base and as it is
167
+ // now, so the red check can find each new test (#581).
168
+ const changed_test_sources: ChangedTestSource[] = []
169
+ for (const file of base_changed) {
170
+ changed_test_sources.push({
171
+ file,
172
+ base: await context.git.fileAt({
173
+ cwd: path,
174
+ sha: base_sha,
175
+ path: file,
176
+ }),
177
+ current: await pathText({ path: join(path, file) }),
178
+ })
179
+ }
180
+ const checked = checkRed({
181
+ criteria_ids: action.criteria_ids,
182
+ mapping: action.mapping,
183
+ baseline,
184
+ current,
185
+ test_files,
186
+ sources,
187
+ changed_test_sources,
188
+ feature_map: await featureMapIn({ context, cwd: path }),
189
+ })
190
+ // A failed prepare after the test-writer's turn is one more problem,
191
+ // for the test-writer's fix loop; so is a turn that changed no file,
192
+ // which would leave the red commit nothing to commit (#494).
193
+ const problems = [
194
+ ...(prepare === null || prepare.ok
195
+ ? []
196
+ : [
197
+ `The prepare command \`${prepare.command}\` failed (${endedText(prepare)}):\n${prepare.output}`,
198
+ ]),
199
+ ...(changed.length > 0 ? [] : [NO_RED_CHANGES]),
200
+ ]
201
+ const result =
202
+ problems.length === 0
203
+ ? checked
204
+ : {
205
+ ...checked,
206
+ ok: false,
207
+ problems: [...problems, ...checked.problems],
208
+ }
209
+ context.journal.append({
210
+ kind: 'red_check',
211
+ ticket: action.ticket,
212
+ role: null,
213
+ content: {
214
+ ...result,
215
+ tests: current,
216
+ ...(exclusive ? { exclusive } : {}),
217
+ },
218
+ })
219
+ }