@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,283 @@
1
+ /**
2
+ * What needs a person: a stuck ticket or run, a skipped or retried ticket,
3
+ * and the replies that resolve them.
4
+ */
5
+ import type { BoardRecord } from './board-vocabulary'
6
+ import {
7
+ RETRY_REFUSED_TEXT,
8
+ reasonText,
9
+ runReasonText,
10
+ } from './reduce-board-reasons'
11
+ import { clip } from './reduce-board-text'
12
+ import {
13
+ DETAIL_MAX,
14
+ addNeedsYou,
15
+ resolveNeedsYou,
16
+ updateFinal,
17
+ updateTicket,
18
+ withTried,
19
+ } from './reduce-board-update'
20
+
21
+ import type { BoardState } from '../shared/board-state'
22
+ import {
23
+ FINAL_KEY,
24
+ RUN_KEY,
25
+ allowedReplies,
26
+ ticketKey,
27
+ } from '../shared/reply-actions'
28
+
29
+ const skipTicket = ({
30
+ state,
31
+ ticket,
32
+ because,
33
+ }: {
34
+ state: BoardState
35
+ ticket: number | null
36
+ because: number | null
37
+ }): BoardState => {
38
+ if (ticket === null) return state
39
+ return resolveNeedsYou({
40
+ state: updateTicket({
41
+ state,
42
+ number: ticket,
43
+ update: (card) => ({
44
+ ...card,
45
+ stage: 'skipped',
46
+ role: null,
47
+ activity:
48
+ because === null
49
+ ? 'skipped'
50
+ : `skipped: waits on skipped #${because}`,
51
+ }),
52
+ }),
53
+ key: ticketKey({ ticket }),
54
+ })
55
+ }
56
+
57
+ /**
58
+ * How the engine took a `retry`: `resume` changes nothing more than the
59
+ * reply did; `restart` builds the edited ticket from scratch (its new
60
+ * snapshot came just before); `refused` leaves it stuck, back in Needs you.
61
+ */
62
+ const ticketRetried = ({
63
+ state,
64
+ record,
65
+ }: {
66
+ state: BoardState
67
+ record: Extract<BoardRecord, { kind: 'ticket_retried' }>
68
+ }): BoardState => {
69
+ const { ticket } = record
70
+ if (ticket === null) return state
71
+ switch (record.content.mode) {
72
+ case 'resume':
73
+ return state
74
+ case 'restart':
75
+ return updateTicket({
76
+ state,
77
+ number: ticket,
78
+ update: (card) => ({
79
+ ...card,
80
+ stage: 'building',
81
+ step: 0,
82
+ role: null,
83
+ fix_round: 0,
84
+ review_fix_round: 0,
85
+ findings: null,
86
+ open_check: null,
87
+ failed_turn: null,
88
+ activity: 'starting over from the edited ticket',
89
+ tried: withTried({
90
+ tried: card.tried,
91
+ line: 'Started over from the edited ticket',
92
+ }),
93
+ }),
94
+ })
95
+ case 'refused': {
96
+ const card = state.tickets.find((entry) => entry.number === ticket)
97
+ return addNeedsYou({
98
+ state: updateTicket({
99
+ state,
100
+ number: ticket,
101
+ update: (entry) => ({
102
+ ...entry,
103
+ stage: 'stuck',
104
+ role: null,
105
+ activity: 'stuck',
106
+ }),
107
+ }),
108
+ item: {
109
+ key: ticketKey({ ticket }),
110
+ ticket,
111
+ subject: `Retry of #${ticket} was refused${card ? `: ${card.title}` : ''}`,
112
+ reason: RETRY_REFUSED_TEXT,
113
+ detail: clip({
114
+ text: record.content.problems.join(' '),
115
+ max: DETAIL_MAX,
116
+ }),
117
+ tried: card?.tried ?? [],
118
+ replies: allowedReplies({
119
+ key: ticketKey({ ticket }),
120
+ ticket,
121
+ }),
122
+ since: record.time,
123
+ },
124
+ })
125
+ }
126
+ }
127
+ }
128
+
129
+ const replyReceived = ({
130
+ state,
131
+ record,
132
+ }: {
133
+ state: BoardState
134
+ record: Extract<BoardRecord, { kind: 'reply_received' }>
135
+ }): BoardState => {
136
+ const { word } = record.content
137
+ const ticket = record.content.ticket ?? record.ticket
138
+ switch (word) {
139
+ case 'retry':
140
+ if (ticket !== null) {
141
+ return resolveNeedsYou({
142
+ state: updateTicket({
143
+ state,
144
+ number: ticket,
145
+ update: (card) => ({
146
+ ...card,
147
+ stage: 'building',
148
+ activity: 'retrying',
149
+ }),
150
+ }),
151
+ key: ticketKey({ ticket }),
152
+ })
153
+ }
154
+ // A bare `retry` while the run is stuck on its budget adds its
155
+ // top-up (one whole budget for a retry journaled with none,
156
+ // #543); stuck for another reason, no budget. The final review
157
+ // is left as it is.
158
+ if (state.needs_you.some((item) => item.key === RUN_KEY)) {
159
+ const top_up =
160
+ state.run_stuck_reason === 'run_budget'
161
+ ? (record.content.top_up ?? state.run_budget_each)
162
+ : 0
163
+ return resolveNeedsYou({
164
+ state: {
165
+ ...state,
166
+ run_budget_tokens: state.run_budget_tokens + top_up,
167
+ run_stuck_reason: null,
168
+ },
169
+ key: RUN_KEY,
170
+ })
171
+ }
172
+ return resolveNeedsYou({
173
+ state: updateFinal({
174
+ state,
175
+ update: (review) => ({ ...review, state: 'reviewing' }),
176
+ }),
177
+ key: FINAL_KEY,
178
+ })
179
+ case 'skip':
180
+ return skipTicket({ state, ticket, because: null })
181
+ case 'ship':
182
+ return resolveNeedsYou({
183
+ state: updateFinal({
184
+ state,
185
+ update: (review) => ({ ...review, state: 'passed' }),
186
+ }),
187
+ key: FINAL_KEY,
188
+ })
189
+ case 'stop':
190
+ return resolveNeedsYou({ state, key: null })
191
+ }
192
+ }
193
+
194
+ /** The record kinds of what needs a person. */
195
+ export const NEEDS_YOU_KINDS = [
196
+ 'ticket_stuck',
197
+ 'run_stuck',
198
+ 'reply_received',
199
+ 'ticket_skipped',
200
+ 'ticket_retried',
201
+ 'reply_ignored',
202
+ ] as const
203
+
204
+ type NeedsYouRecord = Extract<
205
+ BoardRecord,
206
+ { kind: (typeof NEEDS_YOU_KINDS)[number] }
207
+ >
208
+
209
+ /** Applies one record of NEEDS_YOU_KINDS to the board. */
210
+ export const applyNeedsYouKind = ({
211
+ state,
212
+ record,
213
+ }: {
214
+ state: BoardState
215
+ record: NeedsYouRecord
216
+ }): BoardState => {
217
+ const { ticket } = record
218
+ switch (record.kind) {
219
+ case 'ticket_stuck': {
220
+ if (ticket === null) return state
221
+ const card = state.tickets.find((entry) => entry.number === ticket)
222
+ const stuck = updateTicket({
223
+ state,
224
+ number: ticket,
225
+ update: (entry) => ({
226
+ ...entry,
227
+ stage: 'stuck',
228
+ role: null,
229
+ activity: 'stuck',
230
+ }),
231
+ })
232
+ return addNeedsYou({
233
+ state: stuck,
234
+ item: {
235
+ key: ticketKey({ ticket }),
236
+ ticket,
237
+ subject: `Ticket #${ticket} is stuck${card ? `: ${card.title}` : ''}`,
238
+ reason: reasonText({ reason: record.content.reason }),
239
+ detail: clip({
240
+ text: record.content.detail,
241
+ max: DETAIL_MAX,
242
+ }),
243
+ tried: card?.tried ?? [],
244
+ replies: allowedReplies({
245
+ key: ticketKey({ ticket }),
246
+ ticket,
247
+ }),
248
+ since: record.time,
249
+ },
250
+ })
251
+ }
252
+ case 'run_stuck':
253
+ return addNeedsYou({
254
+ state: { ...state, run_stuck_reason: record.content.reason },
255
+ item: {
256
+ key: RUN_KEY,
257
+ ticket: null,
258
+ subject: 'The run is stuck',
259
+ reason: runReasonText({ reason: record.content.reason }),
260
+ detail: clip({
261
+ text: record.content.detail,
262
+ max: DETAIL_MAX,
263
+ }),
264
+ tried: [],
265
+ replies: allowedReplies({ key: RUN_KEY, ticket: null }),
266
+ since: record.time,
267
+ },
268
+ })
269
+ case 'reply_received':
270
+ return replyReceived({ state, record })
271
+ case 'ticket_skipped':
272
+ return skipTicket({
273
+ state,
274
+ ticket,
275
+ because: record.content.because,
276
+ })
277
+ case 'ticket_retried':
278
+ return ticketRetried({ state, record })
279
+ // An ignored reply is only a chat row; the engine answered it.
280
+ case 'reply_ignored':
281
+ return state
282
+ }
283
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Why a ticket, the final review, or the whole run is stuck, as codes and as
3
+ * words: the text a stuck card shows for each reason the engine journals.
4
+ */
5
+ import { LOOP_CAP } from '../shared/board-state'
6
+
7
+ const STUCK_REASONS: Record<string, string> = {
8
+ agent_failed: 'An agent failed on its last try.',
9
+ red_check_failed: 'The red check failed.',
10
+ nothing_new_to_test:
11
+ 'The test-writer says the ticket changes no behavior, so there is nothing new to test. Label it refactor and retry it.',
12
+ leftovers_found:
13
+ 'The leftover scan found files that must not be committed.',
14
+ gates_failed: 'The checks failed.',
15
+ bad_test:
16
+ 'A test was sent back as bad, by the implementer or because it checks nothing.',
17
+ changes_requested: 'The ticket review asked for changes.',
18
+ join_failed: "The ticket couldn't join the run branch.",
19
+ join_gates_failed: 'The checks failed after the ticket joined.',
20
+ install_failed: 'Installing the dependencies failed.',
21
+ prepare_failed: "The repo's prepare command failed.",
22
+ setup_change_needed:
23
+ 'An agent needs a test setup file changed; only you may change one.',
24
+ crashed: 'The engine crashed in the same step again and again.',
25
+ undo_refused:
26
+ 'Undoing its join would have rewound the run branch over other work.',
27
+ }
28
+
29
+ /** Why a retry of an edited ticket was refused, in words. */
30
+ export const RETRY_REFUSED_TEXT =
31
+ "Its new text or labels aren't ready to build."
32
+
33
+ /** A stuck reason code in words. */
34
+ export const reasonText = ({ reason }: { reason: string }): string =>
35
+ STUCK_REASONS[reason] ?? `${reason.replaceAll('_', ' ')}.`
36
+
37
+ /** The final review's stuck reasons that read differently from a ticket's. */
38
+ const FINAL_STUCK_REASONS: Record<string, string> = {
39
+ changes_requested: `The lenses still ask for changes after ${LOOP_CAP} fix rounds.`,
40
+ gates_failed: "The checks still fail on the final review's fixes.",
41
+ bad_test:
42
+ "The implementer sent back a bad test while fixing the final review's findings.",
43
+ }
44
+
45
+ /** A final review's stuck reason code in words. */
46
+ export const finalReasonText = ({ reason }: { reason: string }): string =>
47
+ FINAL_STUCK_REASONS[reason] ?? reasonText({ reason })
48
+
49
+ /** Why the whole run is stuck (`run_stuck`'s reason), in words. */
50
+ const RUN_STUCK_REASONS: Record<string, string> = {
51
+ run_budget:
52
+ 'The run used up its run budget of tokens. `retry` adds half a budget more, and `retry +3M` the amount named.',
53
+ pull_request_failed:
54
+ "The run's pull request didn't open. `retry` tries again, reusing a PR already open from the run branch.",
55
+ }
56
+
57
+ /** A run stuck reason code in words. */
58
+ export const runReasonText = ({ reason }: { reason: string }): string =>
59
+ RUN_STUCK_REASONS[reason] ?? `${reason.replaceAll('_', ' ')}.`
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The run's own records: how it starts and resumes, its repo wait, intake
3
+ * and the spec's snapshot, the run branch, the pull request, and a stop.
4
+ */
5
+ import type { BoardRecord, LeftOutList } from './board-vocabulary'
6
+ import { numbersText } from './reduce-board-text'
7
+ import { updateTicket, withTried } from './reduce-board-update'
8
+
9
+ import {
10
+ DEFAULT_CONFIDENCE_BAR,
11
+ DEFAULT_RUN_BUDGET_TOKENS,
12
+ type BoardState,
13
+ type PersonTicket,
14
+ type RunInfo,
15
+ } from '../shared/board-state'
16
+ import { repoOfIssueUrl } from '../shared/reply-actions'
17
+
18
+ /**
19
+ * The run's version note after a resume on Luca `resumed_on`: both versions
20
+ * when it differs from the one the run started on, else the note so far (a
21
+ * run that once changed version keeps saying so). Nothing when the journal
22
+ * doesn't say what the run started on.
23
+ */
24
+ const versionNote = ({
25
+ run,
26
+ resumed_on,
27
+ }: {
28
+ run: RunInfo
29
+ resumed_on: string
30
+ }): string | null =>
31
+ run.started_on === null || run.started_on === resumed_on
32
+ ? run.version_note
33
+ : `This run started on Luca ${run.started_on} and resumed on Luca ${resumed_on}. It keeps going.`
34
+
35
+ /**
36
+ * The tickets left out for a person (#499), with why in words, as the
37
+ * engine's PR says it.
38
+ *
39
+ * @example
40
+ * personTickets({ left_out: [{ number: 13, title: 'Link', reason: 'waits_on_person', waits_on: [12], through: [] }] })
41
+ * // [{ number: 13, title: 'Link', why: 'waits on #12, which is for a person' }]
42
+ */
43
+ const personTickets = ({
44
+ left_out,
45
+ }: {
46
+ left_out: LeftOutList
47
+ }): PersonTicket[] =>
48
+ left_out.map(({ number, title, reason, waits_on, through }) => {
49
+ if (reason === 'for_a_person') {
50
+ return { number, title, why: 'for a person' }
51
+ }
52
+ const which =
53
+ waits_on.length === 1
54
+ ? 'which is for a person'
55
+ : 'which are for a person'
56
+ const via =
57
+ through.length === 0
58
+ ? ''
59
+ : ` (through ${numbersText({ numbers: through })})`
60
+ return {
61
+ number,
62
+ title,
63
+ why: `waits on ${numbersText({ numbers: waits_on })}, ${which}${via}`,
64
+ }
65
+ })
66
+
67
+ /**
68
+ * The spec's owner and repo from intake's copy of the spec issue, keeping
69
+ * what the board knew when the record lacks them (older journals).
70
+ */
71
+ const specOwner = ({
72
+ run,
73
+ spec,
74
+ }: {
75
+ run: RunInfo
76
+ spec: { author?: string; url?: string }
77
+ }): Pick<RunInfo, 'spec_author' | 'spec_repo'> => ({
78
+ spec_author:
79
+ spec.author === undefined || spec.author === ''
80
+ ? run.spec_author
81
+ : spec.author,
82
+ spec_repo:
83
+ (spec.url === undefined ? null : repoOfIssueUrl({ url: spec.url })) ??
84
+ run.spec_repo,
85
+ })
86
+
87
+ /** The record kinds the run's own records. */
88
+ export const RUN_KINDS = [
89
+ 'run_started',
90
+ 'repo_wait_started',
91
+ 'repo_wait_ended',
92
+ 'engine_resumed',
93
+ 'config_reloaded',
94
+ 'config_reload_refused',
95
+ 'intake_read',
96
+ 'intake_refused',
97
+ 'nothing_to_do',
98
+ 'spec_snapshot',
99
+ 'run_branch_created',
100
+ 'run_stopped',
101
+ 'pull_request_opened',
102
+ 'worktrees_removed',
103
+ ] as const
104
+
105
+ type RunRecord = Extract<BoardRecord, { kind: (typeof RUN_KINDS)[number] }>
106
+
107
+ /** Applies one record of RUN_KINDS to the board. */
108
+ export const applyRunKind = ({
109
+ state,
110
+ record,
111
+ }: {
112
+ state: BoardState
113
+ record: RunRecord
114
+ }): BoardState => {
115
+ const { ticket } = record
116
+ switch (record.kind) {
117
+ case 'run_started': {
118
+ const budget =
119
+ record.content.config?.run_budget_tokens ??
120
+ DEFAULT_RUN_BUDGET_TOKENS
121
+ return {
122
+ ...state,
123
+ run_budget_tokens: budget,
124
+ run_budget_each: budget,
125
+ confidence_bar:
126
+ record.content.config?.models?.confidence_bar ??
127
+ DEFAULT_CONFIDENCE_BAR,
128
+ run: {
129
+ ...state.run,
130
+ spec_number: record.content.spec_number,
131
+ phase: 'intake',
132
+ started_on: record.content.luca_version,
133
+ },
134
+ }
135
+ }
136
+ case 'repo_wait_started':
137
+ return {
138
+ ...state,
139
+ repo_wait: {
140
+ runs: record.content.runs.map(({ run_id }) => run_id),
141
+ since: record.time,
142
+ },
143
+ }
144
+ case 'repo_wait_ended':
145
+ return { ...state, repo_wait: null }
146
+ case 'engine_resumed':
147
+ return {
148
+ ...state,
149
+ run: {
150
+ ...state.run,
151
+ version_note: versionNote({
152
+ run: state.run,
153
+ resumed_on: record.content.luca_version,
154
+ }),
155
+ },
156
+ }
157
+ // A resume's config reload (#516) is only a chat row.
158
+ case 'config_reloaded':
159
+ case 'config_reload_refused':
160
+ return state
161
+ case 'intake_read':
162
+ return {
163
+ ...state,
164
+ run: {
165
+ ...state.run,
166
+ spec_title:
167
+ state.run.spec_title ?? record.content.spec.title,
168
+ phase: 'intake',
169
+ ...specOwner({ run: state.run, spec: record.content.spec }),
170
+ },
171
+ }
172
+ case 'intake_refused':
173
+ return {
174
+ ...state,
175
+ run: {
176
+ ...state.run,
177
+ phase: 'refused',
178
+ refusal: record.content.problems.map(
179
+ (problem) =>
180
+ `${problem.ticket === null ? 'The run' : `#${problem.ticket}`}: missing ${problem.missing.join(', ')}`
181
+ ),
182
+ },
183
+ }
184
+ case 'nothing_to_do':
185
+ return {
186
+ ...state,
187
+ for_a_person: personTickets({
188
+ left_out: record.content.left_out,
189
+ }),
190
+ run: { ...state.run, phase: 'nothing_to_do' },
191
+ }
192
+ case 'spec_snapshot':
193
+ return {
194
+ ...state,
195
+ for_a_person: personTickets({
196
+ left_out: record.content.left_out,
197
+ }),
198
+ run: {
199
+ ...state.run,
200
+ spec_number: record.content.spec.number,
201
+ spec_title: record.content.spec.title,
202
+ phase: 'building',
203
+ ...specOwner({ run: state.run, spec: record.content.spec }),
204
+ },
205
+ }
206
+ case 'run_branch_created':
207
+ return {
208
+ ...state,
209
+ run: { ...state.run, branch: record.content.branch },
210
+ }
211
+ case 'run_stopped': {
212
+ const { reason, role, billing, crashed } = record.content
213
+ const stopped = updateTicket({
214
+ state,
215
+ number: ticket,
216
+ update: (card) => ({
217
+ ...card,
218
+ role: null,
219
+ activity: 'run stopped',
220
+ tried: withTried({
221
+ tried: card.tried,
222
+ line: `${billing ? 'Stopped for billing' : crashed ? 'Stopped after crashes' : 'The run stopped'}: ${reason}`,
223
+ }),
224
+ }),
225
+ })
226
+ return {
227
+ ...stopped,
228
+ run: {
229
+ ...stopped.run,
230
+ stopped: {
231
+ reason,
232
+ role,
233
+ ticket,
234
+ billing,
235
+ crashed,
236
+ since: record.time,
237
+ },
238
+ },
239
+ }
240
+ }
241
+ case 'pull_request_opened':
242
+ return {
243
+ ...state,
244
+ run: {
245
+ ...state.run,
246
+ phase: 'done',
247
+ pr_url: record.content.url,
248
+ pr_number: record.content.number,
249
+ },
250
+ }
251
+ // Removing the worktrees at the end changes nothing on the board.
252
+ case 'worktrees_removed':
253
+ return state
254
+ }
255
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * What depends on the whole board: which unstarted tickets are blocked,
3
+ * whether the final review is active, and the run's status.
4
+ */
5
+ import type {
6
+ BoardState,
7
+ RunStatus,
8
+ TicketCard,
9
+ TicketStage,
10
+ } from '../shared/board-state'
11
+ import { RUN_KEY } from '../shared/reply-actions'
12
+
13
+ /** A ticket's blockers inside this run that aren't done yet. */
14
+ export const unfinishedBlockers = ({
15
+ state,
16
+ ticket,
17
+ }: {
18
+ state: BoardState
19
+ ticket: TicketCard
20
+ }): number[] =>
21
+ ticket.blockers.filter((number) => {
22
+ const blocker = state.tickets.find((card) => card.number === number)
23
+ return blocker !== undefined && blocker.stage !== 'done'
24
+ })
25
+
26
+ /** The stages of a ticket the engine is working on. */
27
+ const ACTIVE_STAGES = new Set<TicketStage>([
28
+ 'building',
29
+ 'reviewing',
30
+ 'testing',
31
+ 'fixing',
32
+ 'joining',
33
+ ])
34
+
35
+ const isActive = ({ state }: { state: BoardState }): boolean =>
36
+ state.tickets.some(
37
+ (ticket) => ticket.started && ACTIVE_STAGES.has(ticket.stage)
38
+ ) ||
39
+ state.final_review.state === 'reviewing' ||
40
+ state.final_review.state === 'fixing'
41
+
42
+ const runStatus = ({ state }: { state: BoardState }): RunStatus => {
43
+ const { phase, engine_ended, stopped } = state.run
44
+ if (phase === 'done' || phase === 'refused' || phase === 'nothing_to_do')
45
+ return phase
46
+ if (stopped) return 'stopped'
47
+ if (engine_ended && !engine_ended.ok) return 'ended_with_error'
48
+ if (state.limit_wait) return 'limit_wait'
49
+ if (state.usage_line_wait) return 'usage_line_wait'
50
+ // Nothing new starts while the run is stuck on its budget.
51
+ if (state.needs_you.some((item) => item.key === RUN_KEY)) return 'stuck'
52
+ if (state.needs_you.length > 0 && !isActive({ state })) return 'stuck'
53
+ return phase
54
+ }
55
+
56
+ /**
57
+ * Recomputes what depends on the whole board: which unstarted tickets are
58
+ * blocked, whether the final review is active, and the run's status.
59
+ */
60
+ export const settle = ({ state }: { state: BoardState }): BoardState => {
61
+ const tickets = state.tickets.map((ticket): TicketCard => {
62
+ if (ticket.started) return ticket
63
+ if (ticket.stage !== 'blocked' && ticket.stage !== 'building')
64
+ return ticket
65
+ const waiting = unfinishedBlockers({ state, ticket })
66
+ return waiting.length > 0
67
+ ? {
68
+ ...ticket,
69
+ stage: 'blocked',
70
+ activity: `waits on ${waiting.map((number) => `#${number}`).join(', ')}`,
71
+ }
72
+ : { ...ticket, stage: 'building', activity: 'queued' }
73
+ })
74
+ const settledTickets = { ...state, tickets }
75
+ const active =
76
+ tickets.length > 0 &&
77
+ tickets.every(
78
+ (ticket) => ticket.stage === 'done' || ticket.stage === 'skipped'
79
+ )
80
+ const withReview = {
81
+ ...settledTickets,
82
+ final_review: { ...state.final_review, active },
83
+ }
84
+ return {
85
+ ...withReview,
86
+ run: { ...withReview.run, status: runStatus({ state: withReview }) },
87
+ }
88
+ }