@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,226 @@
1
+ /**
2
+ * The steps the engine journals: the one a ticket is in, or the ones the
3
+ * run itself is running, and the stage a step puts a ticket in.
4
+ */
5
+ import type { BoardRecord } from './board-vocabulary'
6
+ import { roleWords } from './reduce-board-text'
7
+ import { updateTicket } from './reduce-board-update'
8
+
9
+ import {
10
+ BASELINE_STEP_TEXT,
11
+ type BoardState,
12
+ type TicketCard,
13
+ type TicketStage,
14
+ } from '../shared/board-state'
15
+
16
+ /** A step's action type in words, when it reads better than its name. */
17
+ const STEP_WORDS: Record<string, string> = {
18
+ install_dependencies: 'installing packages',
19
+ run_baseline_tests: BASELINE_STEP_TEXT,
20
+ run_red_check: 'running the red check',
21
+ run_gates: 'running the checks',
22
+ run_final_gates: 'running the checks',
23
+ run_final_slow_tests: 'running the slow tests',
24
+ join_run_branch: 'joining the run branch',
25
+ push_run_branch: 'pushing the run branch',
26
+ push_final_fixes: 'pushing the run branch',
27
+ write_changeset: 'writing the changeset',
28
+ open_pull_request: 'opening the pull request',
29
+ mark_already_done: 'noting the work is already done',
30
+ close_ticket: 'closing the ticket',
31
+ }
32
+
33
+ /**
34
+ * A `step_started`'s step in words: an agent's turn (`<type>:<role>`)
35
+ * waits for its role; any other step as `STEP_WORDS` has it, else its
36
+ * action type.
37
+ *
38
+ * @example
39
+ * stepText({ step: 'run_gates' }) // 'running the checks'
40
+ * stepText({ step: 'follow_up_agent:implementer' }) // 'waiting for the implementer'
41
+ */
42
+ export const stepText = ({ step }: { step: string }): string => {
43
+ const colon = step.indexOf(':')
44
+ if (colon >= 0) {
45
+ return `waiting for the ${roleWords({ role: step.slice(colon + 1) })}`
46
+ }
47
+ return STEP_WORDS[step] ?? step.replaceAll('_', ' ')
48
+ }
49
+
50
+ /** The run's running steps, and the newest of them as its current step. */
51
+ const withRunSteps = ({
52
+ state,
53
+ run_steps,
54
+ }: {
55
+ state: BoardState
56
+ run_steps: BoardState['run_steps']
57
+ }): BoardState => {
58
+ const newest = run_steps.at(-1)
59
+ return {
60
+ ...state,
61
+ run_steps,
62
+ current_step: newest
63
+ ? { text: newest.text, since: newest.since }
64
+ : null,
65
+ }
66
+ }
67
+
68
+ /** No step running anywhere: the engine ended, or the run stopped. */
69
+ export const withoutSteps = ({ state }: { state: BoardState }): BoardState =>
70
+ withRunSteps({
71
+ state: {
72
+ ...state,
73
+ tickets: state.tickets.map((card) =>
74
+ card.current_step === null
75
+ ? card
76
+ : { ...card, current_step: null }
77
+ ),
78
+ },
79
+ run_steps: [],
80
+ })
81
+
82
+ /** The stages a step doesn't move a ticket out of. */
83
+ const SETTLED_STAGES = new Set<TicketStage>(['stuck', 'done', 'skipped'])
84
+
85
+ /**
86
+ * The stage a ticket's step puts it in (#549): the red check and the checks
87
+ * are testing, but the checks after joining are still joining; joining and
88
+ * pushing the run branch are joining. Any other step, or a stuck, done or
89
+ * skipped ticket, keeps its stage.
90
+ */
91
+ const stepStage = ({
92
+ card,
93
+ step,
94
+ }: {
95
+ card: TicketCard
96
+ step: string
97
+ }): TicketStage => {
98
+ if (SETTLED_STAGES.has(card.stage)) return card.stage
99
+ switch (step) {
100
+ case 'run_red_check':
101
+ return 'testing'
102
+ case 'run_gates':
103
+ return card.stage === 'joining' ? 'joining' : 'testing'
104
+ case 'join_run_branch':
105
+ case 'push_run_branch':
106
+ return 'joining'
107
+ default:
108
+ return card.stage
109
+ }
110
+ }
111
+
112
+ /**
113
+ * A step began: it is its ticket's current step, or one of the run's, and
114
+ * it sets the ticket's stage. A new step on the same key (a crash's redo)
115
+ * replaces the one before it.
116
+ */
117
+ const stepStarted = ({
118
+ state,
119
+ record,
120
+ }: {
121
+ state: BoardState
122
+ record: Extract<BoardRecord, { kind: 'step_started' }>
123
+ }): BoardState => {
124
+ // A replayed journal of an engine that ended has no step running.
125
+ const ended = state.run.engine_ended !== null
126
+ const current_step = {
127
+ text: stepText({ step: record.content.step }),
128
+ since: record.time,
129
+ }
130
+ if (record.ticket !== null) {
131
+ return updateTicket({
132
+ state,
133
+ number: record.ticket,
134
+ update: (card) => ({
135
+ ...card,
136
+ stage: stepStage({ card, step: record.content.step }),
137
+ current_step: ended ? card.current_step : current_step,
138
+ // Its next step ends any wait.
139
+ wait: null,
140
+ }),
141
+ })
142
+ }
143
+ if (ended) return state
144
+ const { key } = record.content
145
+ return withRunSteps({
146
+ state,
147
+ run_steps: [
148
+ ...state.run_steps.filter((step) => step.key !== key),
149
+ { key, ...current_step },
150
+ ],
151
+ })
152
+ }
153
+
154
+ const stepEnded = ({
155
+ state,
156
+ record,
157
+ }: {
158
+ state: BoardState
159
+ record: Extract<BoardRecord, { kind: 'step_ended' }>
160
+ }): BoardState => {
161
+ if (record.ticket !== null) {
162
+ return updateTicket({
163
+ state,
164
+ number: record.ticket,
165
+ // Between steps now: what it waits for is worked out on the
166
+ // card, unless the engine journals it (`ticket_waiting`).
167
+ update: (card) => ({
168
+ ...card,
169
+ current_step: null,
170
+ wait: { for: null, on_ticket: null, since: record.time },
171
+ }),
172
+ })
173
+ }
174
+ return withRunSteps({
175
+ state,
176
+ run_steps: state.run_steps.filter(
177
+ (step) => step.key !== record.content.key
178
+ ),
179
+ })
180
+ }
181
+
182
+ /** A ticket began to wait between steps, for what the engine says (#559). */
183
+ const ticketWaiting = ({
184
+ state,
185
+ record,
186
+ }: {
187
+ state: BoardState
188
+ record: Extract<BoardRecord, { kind: 'ticket_waiting' }>
189
+ }): BoardState => {
190
+ const { for: waiting_for, on_ticket, since } = record.content
191
+ return updateTicket({
192
+ state,
193
+ number: record.ticket,
194
+ update: (card) => ({
195
+ ...card,
196
+ wait: { for: waiting_for, on_ticket, since },
197
+ }),
198
+ })
199
+ }
200
+
201
+ /** The record kinds of a step, and of a wait between steps. */
202
+ export const STEP_KINDS = [
203
+ 'step_started',
204
+ 'step_ended',
205
+ 'ticket_waiting',
206
+ ] as const
207
+
208
+ type StepRecord = Extract<BoardRecord, { kind: (typeof STEP_KINDS)[number] }>
209
+
210
+ /** Applies one record of STEP_KINDS to the board. */
211
+ export const applyStepKind = ({
212
+ state,
213
+ record,
214
+ }: {
215
+ state: BoardState
216
+ record: StepRecord
217
+ }): BoardState => {
218
+ switch (record.kind) {
219
+ case 'step_started':
220
+ return stepStarted({ state, record })
221
+ case 'step_ended':
222
+ return stepEnded({ state, record })
223
+ case 'ticket_waiting':
224
+ return ticketWaiting({ state, record })
225
+ }
226
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Words for the reducer: short pure functions that turn ids, counts and
3
+ * record fields into the texts a card or row shows. They hold no board
4
+ * state.
5
+ */
6
+ import type { BoardRecord } from './board-vocabulary'
7
+
8
+ import type { FindingCounts } from '../shared/board-state'
9
+
10
+ /** Longest single "tried" line kept in state. */
11
+ export const LINE_MAX = 240
12
+
13
+ /**
14
+ * A commit's short sha, as git and GitHub show it.
15
+ *
16
+ * @example
17
+ * shortSha({ sha: '3559c25f5a1b' }) // '3559c25'
18
+ */
19
+ export const shortSha = ({ sha }: { sha: string }): string => sha.slice(0, 7)
20
+
21
+ /**
22
+ * An already-done ticket's activity (#484): calm, with the commits that
23
+ * did its work.
24
+ *
25
+ * @example
26
+ * alreadyDoneText({ shas: ['3559c25f5a1b'] }) // 'already done · 3559c25'
27
+ */
28
+ export const alreadyDoneText = ({ shas }: { shas: string[] }): string =>
29
+ ['already done', ...shas.map((sha) => shortSha({ sha }))].join(' · ')
30
+
31
+ /** Cuts `text` to `max` characters, ending in "…" when it was longer. */
32
+ export const clip = ({ text, max }: { text: string; max: number }): string =>
33
+ text.length > max ? `${text.slice(0, max - 1)}…` : text
34
+
35
+ export const firstLine = ({ text }: { text: string }): string =>
36
+ clip({ text: text.split('\n')[0] ?? '', max: LINE_MAX })
37
+
38
+ /** How an agent turn failed, in words: `agent_failed`'s `failure`. */
39
+ const FAILURES: Record<string, string> = {
40
+ agent: 'failed',
41
+ result: 'gave no usable result',
42
+ guard: "broke its role's rules (the changes were undone)",
43
+ engine: 'could not run (an engine-side failure; a fresh agent starts)',
44
+ }
45
+
46
+ /** An `agent_failed` failure kind in words. */
47
+ export const failureText = ({ failure }: { failure: string }): string =>
48
+ FAILURES[failure] ?? `failed (${failure})`
49
+
50
+ /** Issue numbers as `#12`, `#12 and #13`, or `#12, #13 and #14`. */
51
+ export const numbersText = ({ numbers }: { numbers: number[] }): string => {
52
+ const refs = numbers.map((number) => `#${number}`)
53
+ const last = refs.at(-1) ?? ''
54
+ return refs.length <= 1
55
+ ? last
56
+ : `${refs.slice(0, -1).join(', ')} and ${last}`
57
+ }
58
+
59
+ /**
60
+ * A role in words: a final review lens's role (`security-lens`) reads
61
+ * "security lens"; any other role as it is.
62
+ *
63
+ * @example
64
+ * roleWords({ role: 'security-lens' }) // 'security lens'
65
+ */
66
+ export const roleWords = ({ role }: { role: string }): string =>
67
+ role.endsWith('-lens') ? `${role.slice(0, -'-lens'.length)} lens` : role
68
+
69
+ /** A ticket review's findings counted by severity. */
70
+ export const countFindings = ({
71
+ findings,
72
+ }: {
73
+ findings: { severity: 'blocker' | 'should_fix' | 'nit' }[]
74
+ }): FindingCounts => ({
75
+ blocker: findings.filter((finding) => finding.severity === 'blocker')
76
+ .length,
77
+ should_fix: findings.filter((finding) => finding.severity === 'should_fix')
78
+ .length,
79
+ nit: findings.filter((finding) => finding.severity === 'nit').length,
80
+ })
81
+
82
+ const plural = ({ count, word }: { count: number; word: string }) =>
83
+ `${count} ${word}${count === 1 ? '' : 's'}`
84
+
85
+ /**
86
+ * A ticket review's finding counts in words, such as "1 blocker, 2
87
+ * should-fix, 2 nits". Nits are left out unless `nits` is set; `null` when
88
+ * nothing is left to say.
89
+ *
90
+ * @example
91
+ * reviewCountsText({ findings: { blocker: 2, should_fix: 1, nit: 1 }, nits: false })
92
+ * // '2 blockers, 1 should-fix'
93
+ */
94
+ export const reviewCountsText = ({
95
+ findings,
96
+ nits,
97
+ }: {
98
+ findings: FindingCounts
99
+ nits: boolean
100
+ }): string | null => {
101
+ const parts = [
102
+ findings.blocker > 0
103
+ ? plural({ count: findings.blocker, word: 'blocker' })
104
+ : null,
105
+ findings.should_fix > 0 ? `${findings.should_fix} should-fix` : null,
106
+ nits && findings.nit > 0
107
+ ? plural({ count: findings.nit, word: 'nit' })
108
+ : null,
109
+ ].filter((part) => part !== null)
110
+ return parts.length > 0 ? parts.join(', ') : null
111
+ }
112
+
113
+ /** What happened when a ticket was sent back onto the run branch, in words. */
114
+ export const rebasedText = ({
115
+ content,
116
+ }: {
117
+ content: Extract<BoardRecord, { kind: 'ticket_rebased' }>['content']
118
+ }): string => {
119
+ if (content.cause === 'join_gates') {
120
+ return 'The checks failed after joining; fixing on top of the run branch'
121
+ }
122
+ const files = [...content.tests, ...content.code]
123
+ return files.length === 0
124
+ ? 'Clashed with the run branch; fixing on top of it'
125
+ : `Clashed with the run branch in ${files.join(', ')}`
126
+ }
127
+
128
+ /** A test sent back to the test-writer after a rebase (#489), in words. */
129
+ export const testsSentBackText = ({
130
+ content,
131
+ }: {
132
+ content: Extract<BoardRecord, { kind: 'tests_sent_back' }>['content']
133
+ }): string => {
134
+ const test = [content.bad_test.file, content.bad_test.name]
135
+ .filter(Boolean)
136
+ .join(' > ')
137
+ const joined = content.joined.map(({ ticket }) => `#${ticket}`).join(', ')
138
+ return `Test update ${content.round}: ${test || 'a test'} is wrong on the run branch${joined === '' ? '' : ` after ${joined} joined`}; back to the test-writer`
139
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * A ticket's checks while it builds: the red check, commits, the leftover
3
+ * scan, and the checks (the final review's too).
4
+ */
5
+ import type { BoardRecord } from './board-vocabulary'
6
+ import {
7
+ finalTried,
8
+ testCounts,
9
+ updateTicket,
10
+ withTried,
11
+ } from './reduce-board-update'
12
+
13
+ import type { BoardState } from '../shared/board-state'
14
+
15
+ const gatesRun = ({
16
+ state,
17
+ record,
18
+ }: {
19
+ state: BoardState
20
+ record: Extract<BoardRecord, { kind: 'gates_run' }>
21
+ }): BoardState => {
22
+ const { ok, target, checks } = record.content
23
+ const failed = checks
24
+ .filter((check) => !check.ok)
25
+ .map((check) => check.name)
26
+ .join(', ')
27
+ const afterJoin = target === 'run_branch'
28
+ // The final review's slow tests, after round 1's lenses (#563).
29
+ if (target === 'final_slow') {
30
+ return ok
31
+ ? state
32
+ : finalTried({
33
+ state,
34
+ line: `The final review's slow tests failed: ${failed || 'no detail'}`,
35
+ })
36
+ }
37
+ // The final review's fixes are checked on the run branch, with no ticket.
38
+ if (record.ticket === null) {
39
+ return ok
40
+ ? state
41
+ : finalTried({
42
+ state,
43
+ line: `The checks failed on the final review's fixes: ${failed || 'no detail'}`,
44
+ })
45
+ }
46
+ return updateTicket({
47
+ state,
48
+ number: record.ticket,
49
+ update: (card) => {
50
+ if (ok) {
51
+ return afterJoin
52
+ ? { ...card, activity: 'checks passed after joining' }
53
+ : {
54
+ ...card,
55
+ step: 4,
56
+ open_check: null,
57
+ fix_round: 0,
58
+ activity: 'checks passed',
59
+ }
60
+ }
61
+ return {
62
+ ...card,
63
+ step: afterJoin ? card.step : 3,
64
+ open_check: afterJoin ? card.open_check : 'gates',
65
+ activity: afterJoin
66
+ ? 'checks failed after joining'
67
+ : 'checks failed',
68
+ tried: withTried({
69
+ tried: card.tried,
70
+ line: `${afterJoin ? 'After joining, the' : 'The'} checks failed: ${failed || 'no detail'}`,
71
+ }),
72
+ }
73
+ },
74
+ })
75
+ }
76
+
77
+ /** The record kinds of a ticket's checks. */
78
+ export const TICKET_CHECKS_KINDS = [
79
+ 'red_check',
80
+ 'leftover_scan',
81
+ 'commit_made',
82
+ 'gates_run',
83
+ ] as const
84
+
85
+ type TicketChecksRecord = Extract<
86
+ BoardRecord,
87
+ { kind: (typeof TICKET_CHECKS_KINDS)[number] }
88
+ >
89
+
90
+ /** Applies one record of TICKET_CHECKS_KINDS to the board. */
91
+ export const applyTicketChecksKind = ({
92
+ state,
93
+ record,
94
+ }: {
95
+ state: BoardState
96
+ record: TicketChecksRecord
97
+ }): BoardState => {
98
+ const { ticket } = record
99
+ switch (record.kind) {
100
+ case 'red_check':
101
+ return updateTicket({
102
+ state,
103
+ number: ticket,
104
+ update: (card) => ({
105
+ ...card,
106
+ step: record.content.ok ? 2 : 1,
107
+ open_check: record.content.ok ? null : 'red_check',
108
+ fix_round: record.content.ok ? 0 : card.fix_round,
109
+ activity: record.content.ok
110
+ ? 'red check passed'
111
+ : 'red check failed',
112
+ tests: record.content.tests
113
+ ? testCounts({ cases: record.content.tests.cases })
114
+ : card.tests,
115
+ tried: record.content.ok
116
+ ? card.tried
117
+ : withTried({
118
+ tried: card.tried,
119
+ line: `The red check failed: ${record.content.problems[0] ?? 'no detail'}`,
120
+ }),
121
+ }),
122
+ })
123
+ case 'leftover_scan':
124
+ if (record.content.hits.length === 0) return state
125
+ if (ticket === null) {
126
+ return finalTried({
127
+ state,
128
+ line: `The leftover scan found ${record.content.hits.map((hit) => hit.path).join(', ')} in the final review's fixes`,
129
+ })
130
+ }
131
+ return updateTicket({
132
+ state,
133
+ number: ticket,
134
+ update: (card) => ({
135
+ ...card,
136
+ activity: 'leftovers found',
137
+ tried: withTried({
138
+ tried: card.tried,
139
+ line: `The leftover scan found ${record.content.hits.map((hit) => hit.path).join(', ')}`,
140
+ }),
141
+ }),
142
+ })
143
+ case 'commit_made':
144
+ return updateTicket({
145
+ state,
146
+ number: ticket,
147
+ update: (card) => {
148
+ switch (record.content.stage) {
149
+ case 'green':
150
+ return {
151
+ ...card,
152
+ step: 4,
153
+ activity: 'code committed',
154
+ }
155
+ case 'fix':
156
+ return {
157
+ ...card,
158
+ activity: 'review fixes committed',
159
+ }
160
+ default:
161
+ return { ...card, activity: 'tests committed' }
162
+ }
163
+ },
164
+ })
165
+ case 'gates_run':
166
+ return gatesRun({ state, record })
167
+ }
168
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * A ticket's way onto the run branch: joining, being sent back to fix on top
3
+ * of it, being pushed, or being found already done.
4
+ */
5
+ import type { BoardRecord } from './board-vocabulary'
6
+ import {
7
+ alreadyDoneText,
8
+ rebasedText,
9
+ testsSentBackText,
10
+ } from './reduce-board-text'
11
+ import { updateTicket, withTried } from './reduce-board-update'
12
+
13
+ import { ALL_STEPS_DONE, type BoardState } from '../shared/board-state'
14
+
15
+ /**
16
+ * A ticket sent back onto the run branch is fixed on top of it: its tests
17
+ * first if test files clashed, else its code; then the checks and a fresh
18
+ * review, whose fix rounds count from zero again.
19
+ */
20
+ const ticketRebased = ({
21
+ state,
22
+ record,
23
+ }: {
24
+ state: BoardState
25
+ record: Extract<BoardRecord, { kind: 'ticket_rebased' }>
26
+ }): BoardState =>
27
+ updateTicket({
28
+ state,
29
+ number: record.ticket,
30
+ update: (card) => ({
31
+ ...card,
32
+ stage: 'fixing',
33
+ step: record.content.tests.length > 0 ? 0 : 2,
34
+ role: null,
35
+ fix_round: 0,
36
+ // The engine starts the ticket's review loop over on top of the
37
+ // run branch: a fresh reviewer, fresh review fix rounds.
38
+ review_fix_round: 0,
39
+ findings: null,
40
+ open_check: null,
41
+ failed_turn: null,
42
+ activity: 'fixing on the run branch',
43
+ tried: withTried({
44
+ tried: card.tried,
45
+ line: rebasedText({ content: record.content }),
46
+ }),
47
+ }),
48
+ })
49
+
50
+ /** The record kinds of a ticket's way onto the run branch. */
51
+ export const TICKET_JOIN_KINDS = [
52
+ 'ticket_joined',
53
+ 'ticket_rebased',
54
+ 'tests_sent_back',
55
+ 'run_branch_pushed',
56
+ 'ticket_already_done',
57
+ 'ticket_closed',
58
+ ] as const
59
+
60
+ type TicketJoinRecord = Extract<
61
+ BoardRecord,
62
+ { kind: (typeof TICKET_JOIN_KINDS)[number] }
63
+ >
64
+
65
+ /** Applies one record of TICKET_JOIN_KINDS to the board. */
66
+ export const applyTicketJoinKind = ({
67
+ state,
68
+ record,
69
+ }: {
70
+ state: BoardState
71
+ record: TicketJoinRecord
72
+ }): BoardState => {
73
+ const { ticket } = record
74
+ switch (record.kind) {
75
+ case 'ticket_joined':
76
+ return updateTicket({
77
+ state,
78
+ number: ticket,
79
+ // Joined, it is done once its checks there pass and the run
80
+ // branch is pushed.
81
+ update: (card) =>
82
+ record.content.ok
83
+ ? {
84
+ ...card,
85
+ stage: 'joining',
86
+ step: ALL_STEPS_DONE,
87
+ role: null,
88
+ activity: 'joined',
89
+ }
90
+ : {
91
+ ...card,
92
+ activity: 'join failed',
93
+ tried: withTried({
94
+ tried: card.tried,
95
+ line: `Joining the run branch failed: ${record.content.error ?? 'no detail'}`,
96
+ }),
97
+ },
98
+ })
99
+ case 'ticket_rebased':
100
+ return ticketRebased({ state, record })
101
+ case 'tests_sent_back':
102
+ return updateTicket({
103
+ state,
104
+ number: ticket,
105
+ update: (card) => ({
106
+ ...card,
107
+ stage: 'fixing',
108
+ step: 0,
109
+ role: null,
110
+ activity: 'updating tests for the run branch',
111
+ tried: withTried({
112
+ tried: card.tried,
113
+ line: testsSentBackText({ content: record.content }),
114
+ }),
115
+ }),
116
+ })
117
+ case 'run_branch_pushed':
118
+ return updateTicket({
119
+ state,
120
+ number: ticket,
121
+ update: (card) =>
122
+ card.stage === 'joining' || card.stage === 'done'
123
+ ? { ...card, stage: 'done', activity: 'pushed' }
124
+ : card,
125
+ })
126
+ // Done, not stuck (#484): its work was already on the base branch.
127
+ case 'ticket_already_done':
128
+ return updateTicket({
129
+ state,
130
+ number: ticket,
131
+ update: (card) => ({
132
+ ...card,
133
+ stage: 'done',
134
+ step: ALL_STEPS_DONE,
135
+ role: null,
136
+ open_check: null,
137
+ activity: alreadyDoneText({ shas: record.content.shas }),
138
+ }),
139
+ })
140
+ // Its closing changes nothing the board shows.
141
+ case 'ticket_closed':
142
+ return state
143
+ }
144
+ }