@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,108 @@
1
+ /**
2
+ * The build steps that settle the run before any ticket moves: a `stop`
3
+ * reply or an open PR winds the run down (`haltSteps`), and the run branch's
4
+ * install (`installSteps`), which a failure holds every ticket back on. Pure.
5
+ */
6
+ import { stuck, type BuildAction } from './build-action'
7
+ import { notRemoved, type BuildView } from './decide-build-view'
8
+ import { installFailure } from './decide-ticket'
9
+
10
+ import type { RunState } from '../journal/replay'
11
+
12
+ /**
13
+ * The steps that end the run (a `stop` reply, or an open PR), or `null` when
14
+ * the run goes on.
15
+ */
16
+ export const haltSteps = ({
17
+ state,
18
+ view,
19
+ }: {
20
+ state: RunState
21
+ view: BuildView
22
+ }): BuildAction[] | null => {
23
+ const { run_branch, numbers, progress, finished } = view
24
+ const { pull_request } = state
25
+ if (state.stop !== null) {
26
+ // Nothing new starts; finished tickets' worktrees go, the rest stay.
27
+ const paths = notRemoved({
28
+ state,
29
+ paths: numbers.flatMap((number) => {
30
+ const { worktree } = progress(number)
31
+ return finished(number) && worktree !== null
32
+ ? [worktree.path]
33
+ : []
34
+ }),
35
+ })
36
+ if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
37
+ return [{ type: 'done', outcome: 'stopped_by_user' }]
38
+ }
39
+
40
+ if (pull_request !== null) {
41
+ const paths = notRemoved({
42
+ state,
43
+ paths: [
44
+ ...numbers.flatMap((number) => {
45
+ const { worktree } = progress(number)
46
+ return worktree === null ? [] : [worktree.path]
47
+ }),
48
+ run_branch.path,
49
+ ],
50
+ })
51
+ if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
52
+ return [{ type: 'done', outcome: 'pr_opened', pull_request }]
53
+ }
54
+
55
+ return null
56
+ }
57
+
58
+ /**
59
+ * The run branch's install, or the stuck ticket a failed one holds the run on
60
+ * (with the owner's `replies`), or `null` when the run branch is installed.
61
+ */
62
+ export const installSteps = ({
63
+ state,
64
+ view,
65
+ replies,
66
+ }: {
67
+ state: RunState
68
+ view: BuildView
69
+ replies: BuildAction[]
70
+ }): BuildAction[] | null => {
71
+ const { numbers, finished, skipped, isStuck, stuckSteps } = view
72
+ const { run_branch_install } = state
73
+ if (run_branch_install === null) {
74
+ return [
75
+ {
76
+ type: 'install_dependencies',
77
+ target: 'run_branch',
78
+ ticket: null,
79
+ },
80
+ ]
81
+ }
82
+ const runBranchFailure = installFailure({
83
+ install: run_branch_install,
84
+ where: "the run branch's checkout",
85
+ })
86
+
87
+ if (runBranchFailure !== null) {
88
+ // Nothing builds on a run branch whose install failed: the first
89
+ // ticket is stuck on it until a retry installs it again.
90
+ if (numbers.some(isStuck)) {
91
+ return [...numbers.filter(isStuck).flatMap(stuckSteps), ...replies]
92
+ }
93
+ const first = numbers.find(
94
+ (number) => !finished(number) && !skipped(number)
95
+ )
96
+ if (first !== undefined) {
97
+ return [
98
+ stuck({
99
+ ticket: first,
100
+ reason: 'install_failed',
101
+ detail: runBranchFailure,
102
+ }),
103
+ ]
104
+ }
105
+ }
106
+
107
+ return null
108
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The build steps once no ticket can move: closing the already-done tickets
3
+ * when none joined, the final review, the changeset, and the PR
4
+ * (`finishSteps`). Pure.
5
+ */
6
+ import { closeTicketStep } from './already-done'
7
+ import type { BuildAction } from './build-action'
8
+ import { changesetStep } from './changeset'
9
+ import { notRemoved, type BuildView } from './decide-build-view'
10
+ import { decideFinalReview } from './decide-final-review'
11
+ import { finalStuckSteps } from './decide-stuck'
12
+ import { pullRequestText } from './pull-request-text'
13
+
14
+ import type { RunState } from '../journal/replay'
15
+
16
+ /**
17
+ * The steps when no ticket can move now: the already-done tickets closed, the
18
+ * final review, the changeset, and the PR. `replies` are the owner's replies
19
+ * a stuck final review takes.
20
+ */
21
+ export const finishSteps = ({
22
+ state,
23
+ view,
24
+ replies,
25
+ }: {
26
+ state: RunState
27
+ view: BuildView
28
+ replies: BuildAction[]
29
+ }): BuildAction[] => {
30
+ const {
31
+ spec_number,
32
+ snapshot,
33
+ run_branch,
34
+ base_branch,
35
+ run_notes,
36
+ numbers,
37
+ progress,
38
+ pushed,
39
+ alreadyDone,
40
+ finished,
41
+ skipped,
42
+ } = view
43
+
44
+ if (
45
+ numbers.every((number) => finished(number) || skipped(number)) &&
46
+ !numbers.some(pushed)
47
+ ) {
48
+ // No ticket joined the run branch: no PR, and nothing left to build.
49
+ // The engine closes the already-done tickets itself (#484).
50
+ const closing = numbers.filter(
51
+ (number) => alreadyDone(number) && !progress(number).closed
52
+ )
53
+ if (closing.length > 0) {
54
+ return closing.map((number) =>
55
+ closeTicketStep({
56
+ spec_number,
57
+ base_branch,
58
+ ticket: number,
59
+ shas: progress(number).already_done?.shas ?? [],
60
+ })
61
+ )
62
+ }
63
+ const paths = notRemoved({
64
+ state,
65
+ paths: [
66
+ ...numbers.flatMap((number) => {
67
+ const { worktree } = progress(number)
68
+ return worktree === null ? [] : [worktree.path]
69
+ }),
70
+ run_branch.path,
71
+ ],
72
+ })
73
+ if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
74
+ if (numbers.some(skipped)) {
75
+ return [{ type: 'done', outcome: 'all_skipped' }]
76
+ }
77
+ // Every ticket's work was already done: nothing to do.
78
+ return [
79
+ {
80
+ type: 'finish_nothing_to_do',
81
+ closed_tickets: snapshot.closed_tickets,
82
+ already_done: numbers.filter(alreadyDone),
83
+ },
84
+ ]
85
+ }
86
+ if (numbers.every((number) => finished(number) || skipped(number))) {
87
+ const final = decideFinalReview({
88
+ review: state.final_review,
89
+ crashes: state.crashes,
90
+ snapshot,
91
+ run_branch,
92
+ run_branch_gates: state.run_branch_gates,
93
+ run_notes,
94
+ setup_files: state.config?.test_setup_files ?? [],
95
+ })
96
+ if (final.status === 'working') return final.actions
97
+ if (final.status === 'stuck') {
98
+ // The run branch's worktree stays for a retry.
99
+ const paths = notRemoved({
100
+ state,
101
+ paths: numbers.flatMap((number) => {
102
+ const { worktree } = progress(number)
103
+ return worktree === null ? [] : [worktree.path]
104
+ }),
105
+ })
106
+ if (paths.length > 0) return [{ type: 'remove_worktrees', paths }]
107
+ // The owner hears of it, and the run waits for their reply.
108
+ return [
109
+ ...finalStuckSteps({ state, spec_number, run_branch }),
110
+ ...replies,
111
+ ]
112
+ }
113
+ if (state.changesets && state.changeset === null) {
114
+ return [
115
+ {
116
+ type: 'write_changeset',
117
+ branch: run_branch.branch,
118
+ ...changesetStep({ spec: snapshot.spec }),
119
+ },
120
+ ]
121
+ }
122
+ return [
123
+ {
124
+ type: 'open_pull_request',
125
+ head: run_branch.branch,
126
+ base: base_branch,
127
+ ...pullRequestText({
128
+ snapshot,
129
+ tickets: state.tickets,
130
+ final_review: state.final_review,
131
+ memory_saves: state.memory.saved?.saves ?? [],
132
+ }),
133
+ },
134
+ ]
135
+ }
136
+ return [
137
+ {
138
+ type: 'invalid_journal',
139
+ reason: `No ticket can move: ${numbers
140
+ .filter((number) => !finished(number))
141
+ .map((number) => `#${number}`)
142
+ .join(', ')} wait on tickets that never join.`,
143
+ },
144
+ ]
145
+ }
@@ -0,0 +1,87 @@
1
+ import { decide } from './decide'
2
+
3
+ import type { JournalEntry } from '../journal/journal-record'
4
+ import {
5
+ baselineTests,
6
+ commitMade,
7
+ implemented,
8
+ intakePassed,
9
+ leftoverScan,
10
+ practiceTicket,
11
+ redCheck,
12
+ runBranchCreated,
13
+ testsWritten,
14
+ ticketBuilt,
15
+ ticketWorktreeCreated,
16
+ withInstalls,
17
+ worktreeReset,
18
+ } from '../testing/build-fixtures'
19
+ import { recordsFrom } from '../testing/intake-fixtures'
20
+ import { REFACTOR_LABEL } from '../tracker/tracker'
21
+
22
+ /**
23
+ * Helpers shared by the tests of the decision steps for building a ticket
24
+ * (`decide-build.test.ts` and the `decide-build-*.test.ts` files beside it).
25
+ */
26
+
27
+ export const TICKET = practiceTicket({ number: 11 })
28
+
29
+ /**
30
+ * Decide on a run with one ticket (#11) and these entries after intake. Each
31
+ * new worktree gets a passing install unless the entries list one.
32
+ */
33
+ export const decideAfter = (entries: JournalEntry[]) =>
34
+ decide({
35
+ records: recordsFrom({
36
+ entries: [
37
+ ...intakePassed({ tickets: [TICKET] }),
38
+ ...withInstalls({ entries }),
39
+ ],
40
+ }),
41
+ })
42
+
43
+ /** The steps of ticket #11 up to and including the step at `index`. */
44
+ export const stepsUpTo = (index: number) => [
45
+ runBranchCreated(),
46
+ ...ticketBuilt({ ticket: 11 }).slice(0, index + 1),
47
+ ]
48
+
49
+ /** Ticket #11 up to its first bad-test bounce and the worktree reset. */
50
+ export const bounced = (): JournalEntry[] => [
51
+ ...stepsUpTo(5),
52
+ implemented({ ticket: 11, outcome: 'bad_test' }),
53
+ worktreeReset({ ticket: 11 }),
54
+ ]
55
+
56
+ /** After the bounce: fresh tests, a passing red check, the second red commit. */
57
+ export const replaced = (): JournalEntry[] => [
58
+ ...bounced(),
59
+ testsWritten({ ticket: 11, session_id: 'tw-2' }),
60
+ redCheck({ ticket: 11, ok: true }),
61
+ leftoverScan({ ticket: 11, stage: 'red' }),
62
+ commitMade({ ticket: 11, stage: 'red' }),
63
+ ]
64
+
65
+ export const REFACTOR_TICKET = practiceTicket({
66
+ number: 11,
67
+ title: 'Split sum into its own file',
68
+ labels: ['ready-for-agent', REFACTOR_LABEL],
69
+ })
70
+
71
+ /** Decide on a run whose one ticket (#11) is a refactor ticket. */
72
+ export const decideRefactorAfter = (entries: JournalEntry[]) =>
73
+ decide({
74
+ records: recordsFrom({
75
+ entries: [
76
+ ...intakePassed({ tickets: [REFACTOR_TICKET] }),
77
+ ...withInstalls({
78
+ entries: [
79
+ runBranchCreated(),
80
+ ticketWorktreeCreated({ ticket: 11 }),
81
+ ],
82
+ }),
83
+ baselineTests({ ticket: 11 }),
84
+ ...entries,
85
+ ],
86
+ }),
87
+ })
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Each ticket's next build step (`ticketSteps`): its worktree, the step its
3
+ * progress calls for, a skip, a crash, or its stuck steps, with the join queue
4
+ * deciding which approved ticket joins the run branch. A ticket with no step
5
+ * now because it waits on purpose (its `TicketWait`) waits for another
6
+ * ticket's shared baseline, the join queue, or the start queue. Pure.
7
+ */
8
+ import sortBy from 'lodash/sortBy'
9
+
10
+ import { stuck, type BuildAction } from './build-action'
11
+ import type { BuildView } from './decide-build-view'
12
+ import { crashDetail, crashedOut } from './decide-crashes'
13
+ import { skipDependent } from './decide-stuck'
14
+ import {
15
+ baselineRunner,
16
+ nextTicketStep,
17
+ sharedBaselineStep,
18
+ } from './decide-ticket'
19
+
20
+ import type { RunState, TicketProgress } from '../journal/replay'
21
+
22
+ /**
23
+ * A ticket held on purpose between steps (#559): for `baseline`, another
24
+ * ticket's shared baseline tests; for `join`, the join queue; for `start`,
25
+ * the start queue; each on `on_ticket`.
26
+ */
27
+ export type TicketWait = {
28
+ ticket: number
29
+ for: 'baseline' | 'join' | 'start'
30
+ on_ticket: number | null
31
+ }
32
+
33
+ /** A ticket's next steps, or the wait that holds it. */
34
+ type TicketPlan = { steps: BuildAction[]; wait: TicketWait | null }
35
+
36
+ const NOTHING: TicketPlan = { steps: [], wait: null }
37
+
38
+ /**
39
+ * Whether a ticket's review finally approved it (no fix round open) and it
40
+ * has not pushed yet: it waits in the join queue, or is joining.
41
+ */
42
+ const inJoinQueue = (progress: TicketProgress): boolean =>
43
+ progress.approved_seq !== null &&
44
+ progress.review_fix === null &&
45
+ progress.pushed === null
46
+
47
+ /** Each ticket's plan: its next step, at most one, or what it waits for. */
48
+ const ticketPlans = ({
49
+ state,
50
+ view,
51
+ }: {
52
+ state: RunState
53
+ view: BuildView
54
+ }): TicketPlan[] => {
55
+ const {
56
+ spec_number,
57
+ snapshot,
58
+ run_branch,
59
+ base_branch,
60
+ run_notes,
61
+ numbers,
62
+ progress,
63
+ skipped,
64
+ finished,
65
+ isStuck,
66
+ stuckSteps,
67
+ } = view
68
+
69
+ // A ticket already on the run branch finishes its join first (#519): a
70
+ // retried ticket approved earlier never joins on top of it.
71
+ const queue = sortBy(
72
+ numbers.filter(
73
+ (number) =>
74
+ inJoinQueue(progress(number)) &&
75
+ !isStuck(number) &&
76
+ !skipped(number)
77
+ ),
78
+ [
79
+ (number) => progress(number).joined === null,
80
+ (number) => progress(number).approved_seq,
81
+ ]
82
+ )
83
+ // A stuck ticket's join is undone before any new ticket starts on it
84
+ // or joins on top of it (#519): a join on top would be undone with it.
85
+ const stuckJoinOnRunBranch = numbers.some(
86
+ (number) =>
87
+ isStuck(number) &&
88
+ progress(number).joined?.ok === true &&
89
+ progress(number).pushed === null
90
+ )
91
+ const joiner = stuckJoinOnRunBranch ? null : (queue[0] ?? null)
92
+ const can_move = (other: number) =>
93
+ !finished(other) && !skipped(other) && !isStuck(other)
94
+
95
+ return numbers.map((number): TicketPlan => {
96
+ const ticket = snapshot.tickets[number]
97
+ const ticketProgress = progress(number)
98
+ if (ticket === undefined || finished(number)) return NOTHING
99
+ if (skipped(number)) return NOTHING
100
+ // The same step cut off by crashes too often is not taken again.
101
+ const crash = crashedOut({
102
+ crashes: state.crashes,
103
+ keys: (key) => key === String(number),
104
+ })
105
+ if (crash !== null) {
106
+ return {
107
+ steps: [
108
+ stuck({
109
+ ticket: number,
110
+ reason: 'crashed',
111
+ detail: crashDetail({ crash }),
112
+ }),
113
+ ],
114
+ wait: null,
115
+ }
116
+ }
117
+ if (isStuck(number)) return { steps: stuckSteps(number), wait: null }
118
+ const skippedBlocker = ticket.blockers.find(skipped)
119
+ if (skippedBlocker !== undefined) {
120
+ return {
121
+ steps: [
122
+ skipDependent({
123
+ ticket: number,
124
+ because: skippedBlocker,
125
+ spec_number,
126
+ }),
127
+ ],
128
+ wait: null,
129
+ }
130
+ }
131
+ if (ticketProgress.worktree === null) {
132
+ const unblocked =
133
+ !stuckJoinOnRunBranch && ticket.blockers.every(finished)
134
+ if (unblocked && queue.length === 0) {
135
+ return {
136
+ steps: [
137
+ {
138
+ type: 'create_ticket_worktree',
139
+ ticket: number,
140
+ run_branch: run_branch.branch,
141
+ },
142
+ ],
143
+ wait: null,
144
+ }
145
+ }
146
+ // Unblocked, it starts once the join queue is empty.
147
+ return unblocked
148
+ ? {
149
+ steps: [],
150
+ wait: {
151
+ ticket: number,
152
+ for: 'start',
153
+ on_ticket: queue[0] ?? null,
154
+ },
155
+ }
156
+ : NOTHING
157
+ }
158
+ const baseline = { number, numbers, progress, can_move }
159
+ const step = nextTicketStep({
160
+ snapshot,
161
+ ticket,
162
+ progress: ticketProgress,
163
+ run_notes,
164
+ run_branch,
165
+ base_branch,
166
+ joiner: number === joiner,
167
+ setup_files: state.config?.test_setup_files ?? [],
168
+ baseline_step:
169
+ ticketProgress.baseline === null
170
+ ? sharedBaselineStep(baseline)
171
+ : null,
172
+ })
173
+ if (step !== null) return { steps: [step], wait: null }
174
+ if (ticketProgress.baseline === null) {
175
+ return {
176
+ steps: [],
177
+ wait: {
178
+ ticket: number,
179
+ for: 'baseline',
180
+ on_ticket: baselineRunner(baseline),
181
+ },
182
+ }
183
+ }
184
+ return inJoinQueue(ticketProgress) && number !== joiner
185
+ ? {
186
+ steps: [],
187
+ wait: { ticket: number, for: 'join', on_ticket: joiner },
188
+ }
189
+ : NOTHING
190
+ })
191
+ }
192
+
193
+ /**
194
+ * The next step of each ticket that can move now, at most one per ticket,
195
+ * and the tickets that wait on purpose between steps now, in ticket order:
196
+ * one decision, so a wait is only ever one that left its ticket no step.
197
+ */
198
+ export const ticketSteps = (args: {
199
+ state: RunState
200
+ view: BuildView
201
+ }): { steps: BuildAction[]; waits: TicketWait[] } => {
202
+ const plans = ticketPlans(args)
203
+ return {
204
+ steps: plans.flatMap(({ steps }) => steps),
205
+ waits: plans.flatMap(({ wait }) => (wait === null ? [] : [wait])),
206
+ }
207
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * What the build half of the decision step reads about a run's tickets: the
3
+ * tickets in order, each one's progress, whether it is skipped, pushed,
4
+ * already done, finished, or stuck, and its stuck steps (`buildViewOf`). Pure.
5
+ */
6
+ import type { BuildAction } from './build-action'
7
+ import { newestRunNotes } from './build-launch'
8
+ import { stuckTicketSteps } from './decide-stuck'
9
+
10
+ import {
11
+ EMPTY_TICKET_PROGRESS,
12
+ type ReplayedRunNote,
13
+ type ReplayedSnapshot,
14
+ type ReplayedWorktree,
15
+ type RunState,
16
+ type TicketProgress,
17
+ } from '../journal/replay'
18
+
19
+ /** The worktrees not removed yet, of these paths. */
20
+ export const notRemoved = ({
21
+ paths,
22
+ state,
23
+ }: {
24
+ paths: string[]
25
+ state: RunState
26
+ }): string[] => paths.filter((path) => !state.removed_worktrees.includes(path))
27
+
28
+ /** A run's tickets, as the build half of the decision step reads them. */
29
+ export type BuildView = {
30
+ spec_number: number
31
+ snapshot: ReplayedSnapshot
32
+ run_branch: ReplayedWorktree
33
+ base_branch: string
34
+ run_notes: ReplayedRunNote[]
35
+ /** The run's tickets that intake snapshotted, in order. */
36
+ numbers: number[]
37
+ progress: (number: number) => TicketProgress
38
+ skipped: (number: number) => boolean
39
+ pushed: (number: number) => boolean
40
+ alreadyDone: (number: number) => boolean
41
+ /** Pushed, or its work was already on the base branch (#484). */
42
+ finished: (number: number) => boolean
43
+ isStuck: (number: number) => boolean
44
+ /** The steps that tell the owner a ticket is stuck, and take its reply. */
45
+ stuckSteps: (number: number) => BuildAction[]
46
+ }
47
+
48
+ /** The view of a run that has a run branch and a snapshot. */
49
+ export const buildViewOf = ({
50
+ state,
51
+ spec_number,
52
+ snapshot,
53
+ run_branch,
54
+ }: {
55
+ state: RunState
56
+ spec_number: number
57
+ snapshot: ReplayedSnapshot
58
+ run_branch: ReplayedWorktree
59
+ }): BuildView => {
60
+ const base_branch = state.base_branch ?? 'main'
61
+ const run_notes = newestRunNotes({ run_notes: state.run_notes })
62
+ const numbers = snapshot.ticket_order.filter(
63
+ (number) => snapshot.tickets[number] !== undefined
64
+ )
65
+ const progress = (number: number): TicketProgress =>
66
+ state.tickets[number] ?? EMPTY_TICKET_PROGRESS
67
+
68
+ const skipped = (number: number) => progress(number).skipped !== null
69
+ const pushed = (number: number) =>
70
+ !numbers.includes(number) || progress(number).pushed !== null
71
+ const alreadyDone = (number: number) =>
72
+ progress(number).already_done !== null
73
+ /** Pushed, or its work was already on the base branch (#484). */
74
+ const finished = (number: number) => pushed(number) || alreadyDone(number)
75
+ const stuckSteps = (number: number): BuildAction[] => {
76
+ const ticket = snapshot.tickets[number]
77
+ return ticket === undefined
78
+ ? []
79
+ : stuckTicketSteps({
80
+ spec_number,
81
+ ticket,
82
+ progress: progress(number),
83
+ })
84
+ }
85
+ const isStuck = (number: number) =>
86
+ progress(number).stuck !== null && !skipped(number)
87
+
88
+ return {
89
+ spec_number,
90
+ snapshot,
91
+ run_branch,
92
+ base_branch,
93
+ run_notes,
94
+ numbers,
95
+ progress,
96
+ skipped,
97
+ pushed,
98
+ alreadyDone,
99
+ finished,
100
+ isStuck,
101
+ stuckSteps,
102
+ }
103
+ }