@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,350 @@
1
+ import type { BoardRecord } from './board-vocabulary'
2
+ import {
3
+ clip,
4
+ countFindings,
5
+ numbersText,
6
+ reviewCountsText,
7
+ startKind,
8
+ } from './reduce-board'
9
+
10
+ import type { Tone } from '../shared/board-rows'
11
+ import { LOOP_CAP, type BoardState } from '../shared/board-state'
12
+
13
+ /**
14
+ * The words for single records that need more than a line: an agent's start
15
+ * and finish (a ticket's, the final review's, the learner's), a message, a
16
+ * memory search or save, and why a reply was ignored. Pure.
17
+ */
18
+
19
+ /** What a commit's `stage` committed, in words. */
20
+ export const COMMITTED: Record<string, string> = {
21
+ red: 'tests',
22
+ green: 'code',
23
+ fix: 'review fixes',
24
+ }
25
+
26
+ export const on = ({ ticket }: { ticket: number | null }): string =>
27
+ ticket === null ? '' : `#${ticket}: `
28
+
29
+ export const event = ({ text, tone }: { text: string; tone: Tone }) => ({
30
+ text,
31
+ tone,
32
+ })
33
+
34
+ export const lowerFirst = ({ text }: { text: string }): string =>
35
+ text.replace(/^./, (first) => first.toLowerCase())
36
+
37
+ /** Why the engine couldn't use a reply (`reply_ignored`'s `reason`), in words. */
38
+ export const ignoredText = ({
39
+ reason,
40
+ before,
41
+ }: {
42
+ reason: string
43
+ before: BoardState
44
+ }): string => {
45
+ switch (reason) {
46
+ case 'no_ticket_named': {
47
+ const stuck = before.needs_you.find((item) => item.ticket !== null)
48
+ return `name the ticket, like \`retry #${stuck?.ticket ?? 'n'}\``
49
+ }
50
+ case 'not_stuck':
51
+ return "the ticket it names isn't stuck"
52
+ case 'nothing_stuck':
53
+ return 'nothing is stuck right now'
54
+ case 'ship_needs_final_review':
55
+ return '`ship` is only for a stuck final review'
56
+ case 'amount_only_for_run_budget':
57
+ return 'an amount is only for a run stuck on its budget'
58
+ case 'bad_amount':
59
+ return 'the amount is not a whole number above zero'
60
+ default:
61
+ return reason.replaceAll('_', ' ')
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Why a run ended with nothing to do: its tickets were already done (#484),
67
+ * or what is open is for a person or waits on one (#499), or the spec has
68
+ * no open tickets.
69
+ */
70
+ export const nothingToDoText = ({
71
+ content,
72
+ }: {
73
+ content: Extract<BoardRecord, { kind: 'nothing_to_do' }>['content']
74
+ }): string => {
75
+ const numbersOf = (reason: 'for_a_person' | 'waits_on_person') =>
76
+ content.left_out
77
+ .filter((ticket) => ticket.reason === reason)
78
+ .map(({ number }) => number)
79
+ const people = numbersOf('for_a_person')
80
+ const waiting = numbersOf('waits_on_person')
81
+ const done = content.already_done
82
+ const parts = [
83
+ done.length === 0
84
+ ? ''
85
+ : `the work of ${done.map((number) => `#${number}`).join(', ')} was already on the base branch`,
86
+ people.length === 0
87
+ ? ''
88
+ : `${numbersText({ numbers: people })} ${people.length === 1 ? 'is' : 'are'} for a person`,
89
+ waiting.length === 0
90
+ ? ''
91
+ : `${numbersText({ numbers: waiting })} ${waiting.length === 1 ? 'waits' : 'wait'} on a ticket for a person`,
92
+ ].filter((part) => part !== '')
93
+ if (parts.length === 0)
94
+ return 'Nothing to do: the spec has no open tickets.'
95
+ const last = parts.at(-1) ?? ''
96
+ const text =
97
+ parts.length === 1
98
+ ? last
99
+ : `${parts.slice(0, -1).join(', ')}, and ${last}`
100
+ return `Nothing to do: ${text}.`
101
+ }
102
+
103
+ /** Longest message line shown in a row; the whole message is in the journal. */
104
+ const MESSAGE_LINE_MAX = 120
105
+
106
+ export const messageText = ({
107
+ record,
108
+ }: {
109
+ record: Extract<BoardRecord, { kind: 'agent_message' }>
110
+ }): { text: string; tone: Tone } => {
111
+ const { from, to, text, status, reason } = record.content
112
+ const why = reason ?? 'no reason given'
113
+ if (status === 'refused') {
114
+ return event({
115
+ text: `${from}'s message to ${to} was refused: ${why}`,
116
+ tone: 'warning',
117
+ })
118
+ }
119
+ const shown = clip({
120
+ text: text.split('\n')[0] ?? '',
121
+ max: MESSAGE_LINE_MAX,
122
+ })
123
+ return status === 'queued'
124
+ ? event({ text: `${from} → ${to}: ${shown}`, tone: 'info' })
125
+ : event({
126
+ text: `${from} → ${to}: ${shown} (not delivered: ${why})`,
127
+ tone: 'warning',
128
+ })
129
+ }
130
+
131
+ const RECALL_POINTS: Record<string, string> = {
132
+ run_start: "the run's start",
133
+ ticket: 'a ticket',
134
+ review: 'a review',
135
+ fix_round: 'a fix round',
136
+ }
137
+
138
+ /**
139
+ * A memory search (#370) adds a row only when a vault failed, since the
140
+ * run goes on without it; the panel counts the rest.
141
+ */
142
+ export const recalledText = ({
143
+ record,
144
+ }: {
145
+ record: Extract<BoardRecord, { kind: 'memory_recalled' }>
146
+ }): { text: string; tone: Tone } | null => {
147
+ const failed = record.content.vaults.filter(({ ok }) => !ok)
148
+ if (failed.length === 0) return null
149
+ const where = RECALL_POINTS[record.content.point] ?? record.content.point
150
+ const at = on({ ticket: record.ticket })
151
+ return event({
152
+ text: `${at}memory search for ${where} failed in ${failed.map(({ vault }) => vault).join(', ')}: ${failed[0]?.error?.split('\n')[0] ?? 'no reason given'} The run goes on.`,
153
+ tone: 'warning',
154
+ })
155
+ }
156
+
157
+ /** The learner's answer (#370): how many memories it proposed, and helped. */
158
+ export const learnerFinishedText = ({
159
+ record,
160
+ }: {
161
+ record: Extract<BoardRecord, { kind: 'agent_finished' }>
162
+ }): { text: string; tone: Tone } => {
163
+ const proposed = record.content.result.memories?.length ?? 0
164
+ const helped = record.content.result.helped?.length ?? 0
165
+ return event({
166
+ text: `The learner proposed ${proposed} ${proposed === 1 ? 'memory' : 'memories'}; ${helped} shown ${helped === 1 ? 'memory' : 'memories'} helped.`,
167
+ tone: 'info',
168
+ })
169
+ }
170
+
171
+ /** What became of the learner's memories (#370), counted. */
172
+ export const savedText = ({
173
+ record,
174
+ }: {
175
+ record: Extract<BoardRecord, { kind: 'memories_saved' }>
176
+ }): { text: string; tone: Tone } => {
177
+ const count = (outcome: string) =>
178
+ record.content.saves.filter((save) => save.outcome === outcome).length
179
+ const failed = count('failed')
180
+ return event({
181
+ text: `Memories saved: ${count('added')} added, ${count('updated')} updated, ${count('refused')} refused, ${failed} failed.`,
182
+ tone: failed > 0 ? 'warning' : 'success',
183
+ })
184
+ }
185
+
186
+ /** A `ship` reply to the stuck final review, in words. */
187
+ export const SHIPPED_TEXT =
188
+ 'You replied `ship`: the PR opens with the open findings listed at the top.'
189
+
190
+ /**
191
+ * A final review agent's start (no ticket). A lens's start adds no row (its
192
+ * `lens_finished` says what it found); a fixer's says what it is fixing.
193
+ */
194
+ export const finalStartedText = ({
195
+ record,
196
+ }: {
197
+ record: Extract<BoardRecord, { kind: 'agent_started' }>
198
+ }): { text: string; tone: Tone } | null => {
199
+ const { role, follow_up_of } = record.content
200
+ if (role.endsWith('-lens')) return null
201
+ return follow_up_of === null
202
+ ? event({
203
+ text: `Final review: a fresh ${role} fixes the lenses' findings on the whole run branch.`,
204
+ tone: 'warning',
205
+ })
206
+ : event({
207
+ text: `Final review: the ${role} got the failure back.`,
208
+ tone: 'warning',
209
+ })
210
+ }
211
+
212
+ /**
213
+ * A fixer's answers counted: fixed, won't fix, and passed to the other
214
+ * writer role (`other_role`, #616), the last only when there are any.
215
+ */
216
+ const answerCounts = ({
217
+ responses,
218
+ }: {
219
+ responses: { response: 'fixed' | 'wont_fix' | 'other_role' }[]
220
+ }): string => {
221
+ const count = (answer: string) =>
222
+ responses.filter(({ response }) => response === answer).length
223
+ const passed = count('other_role')
224
+ const counts = `${count('fixed')} fixed, ${count('wont_fix')} won't fix`
225
+ return passed === 0 ? counts : `${counts}, ${passed} for the other role`
226
+ }
227
+
228
+ /** A final review agent's finish (no ticket): a fixer's answers, or none for a lens. */
229
+ export const finalFinishedText = ({
230
+ record,
231
+ }: {
232
+ record: Extract<BoardRecord, { kind: 'agent_finished' }>
233
+ }): { text: string; tone: Tone } | null => {
234
+ const { role, result } = record.content
235
+ if (role.endsWith('-lens')) return null
236
+ const responses = result.finding_responses
237
+ if (responses.length === 0) {
238
+ return event({
239
+ text: `Final review: the ${role} finished.`,
240
+ tone: 'info',
241
+ })
242
+ }
243
+ return event({
244
+ text: `Final review: the ${role} answered the findings: ${answerCounts({ responses })}.`,
245
+ tone: 'info',
246
+ })
247
+ }
248
+
249
+ export const startedText = ({
250
+ before,
251
+ after,
252
+ record,
253
+ }: {
254
+ before: BoardState
255
+ after: BoardState
256
+ record: Extract<BoardRecord, { kind: 'agent_started' }>
257
+ }): { text: string; tone: Tone } | null => {
258
+ const at = on({ ticket: record.ticket })
259
+ const { role, follow_up_of } = record.content
260
+ const card = before.tickets.find(({ number }) => number === record.ticket)
261
+ const next = after.tickets.find(({ number }) => number === record.ticket)
262
+ const round = next?.fix_round
263
+ switch (startKind({ card, role, follow_up_of })) {
264
+ case 'review_fix':
265
+ return event({
266
+ text: `${at}review fix round ${next?.review_fix_round ?? 1}/${LOOP_CAP}: ${
267
+ role === 'test-writer'
268
+ ? 'a fresh test-writer fixes the test findings.'
269
+ : `the ${role} got the findings back.`
270
+ }`,
271
+ tone: 'warning',
272
+ })
273
+ case 'fix':
274
+ return event({
275
+ text: `${at}fix round ${round ?? 0}/${LOOP_CAP}: the ${role} got the failure back.`,
276
+ tone: 'warning',
277
+ })
278
+ case 'resent':
279
+ return null
280
+ case 'retry':
281
+ return event({
282
+ text: `${at}the ${role} tries again.`,
283
+ tone: 'warning',
284
+ })
285
+ case 'fresh':
286
+ return role === 'ticket-reviewer' &&
287
+ card !== undefined &&
288
+ card.review_fix_round > 0
289
+ ? event({
290
+ text: `${at}re-review ${next?.review_round ?? 0} of the new changes.`,
291
+ tone: 'info',
292
+ })
293
+ : event({ text: `${at}the ${role} started.`, tone: 'info' })
294
+ }
295
+ }
296
+
297
+ export const finishedText = ({
298
+ before,
299
+ record,
300
+ }: {
301
+ before: BoardState
302
+ record: Extract<BoardRecord, { kind: 'agent_finished' }>
303
+ }): { text: string; tone: Tone } => {
304
+ const at = on({ ticket: record.ticket })
305
+ const { role, result } = record.content
306
+ const card = before.tickets.find(({ number }) => number === record.ticket)
307
+ if (card?.open_check === 'review' && role !== 'ticket-reviewer') {
308
+ return event({
309
+ text: `${at}the ${role} answered the findings: ${answerCounts({ responses: result.finding_responses })}.`,
310
+ tone: 'info',
311
+ })
312
+ }
313
+ if (role === 'test-writer') {
314
+ if (result.outcome === 'already_done') {
315
+ return event({
316
+ text: `${at}the test-writer found the work already done.`,
317
+ tone: 'info',
318
+ })
319
+ }
320
+ return result.outcome === 'nothing_new_to_test'
321
+ ? event({ text: `${at}nothing new to test.`, tone: 'info' })
322
+ : event({ text: `${at}tests written.`, tone: 'info' })
323
+ }
324
+ if (role === 'implementer') {
325
+ return result.outcome === 'bad_test'
326
+ ? event({
327
+ text: `${at}the implementer sent back a bad test.`,
328
+ tone: 'warning',
329
+ })
330
+ : event({ text: `${at}code written.`, tone: 'info' })
331
+ }
332
+ if (role === 'ticket-reviewer') {
333
+ const counts = result.findings
334
+ ? reviewCountsText({
335
+ findings: countFindings({ findings: result.findings }),
336
+ nits: true,
337
+ })
338
+ : null
339
+ return result.verdict === 'approve'
340
+ ? event({
341
+ text: `${at}the ticket review approved it${counts ? ` (${counts})` : ''}.`,
342
+ tone: 'success',
343
+ })
344
+ : event({
345
+ text: `${at}the ticket review asked for changes${counts ? `: ${counts}` : ''}.`,
346
+ tone: 'warning',
347
+ })
348
+ }
349
+ return event({ text: `${at}the ${role} finished.`, tone: 'info' })
350
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * An agent's start on a ticket: what kind of start it is (fresh, a retry, a
3
+ * fix round, a review fix round), the card's stage, step and activity for
4
+ * each role, and the model the agent runs on.
5
+ */
6
+ import type { BoardRecord } from './board-vocabulary'
7
+ import { reviewCountsText } from './reduce-board-text'
8
+ import { updateTicket, withTried } from './reduce-board-update'
9
+
10
+ import {
11
+ LOOP_CAP,
12
+ type BoardState,
13
+ type TicketCard,
14
+ } from '../shared/board-state'
15
+
16
+ /** The roles that fix a ticket review's findings. */
17
+ const REVIEW_FIXERS = new Set(['test-writer', 'implementer'])
18
+
19
+ /**
20
+ * What an `agent_started` is, from the ticket's card before it:
21
+ * - `retry`: a new try after the role's (or any) turn failed;
22
+ * - `review_fix`: a test-writer (always fresh) or the implementer (a
23
+ * follow-up, or fresh if its session was lost) sent a ticket review's
24
+ * findings, in a review fix round;
25
+ * - `fix`: a follow-up answering a failed red check or gates, a new round;
26
+ * - `resent`: the same fix sent again (its turn never ended, such as after
27
+ * a crash);
28
+ * - `fresh`: anything else.
29
+ */
30
+ export const startKind = ({
31
+ card,
32
+ role,
33
+ follow_up_of,
34
+ }: {
35
+ card: TicketCard | undefined
36
+ role: string
37
+ follow_up_of: string | null
38
+ }): 'retry' | 'review_fix' | 'fix' | 'resent' | 'fresh' => {
39
+ if (card === undefined) return 'fresh'
40
+ if (card.failed_turn !== null) return 'retry'
41
+ if (card.open_check === 'review' && REVIEW_FIXERS.has(role)) {
42
+ return card.role === null ? 'review_fix' : 'resent'
43
+ }
44
+ if (follow_up_of === null || card.open_check === null) return 'fresh'
45
+ return card.role === null ? 'fix' : 'resent'
46
+ }
47
+
48
+ const OPEN_CHECK_TEXT = {
49
+ red_check: 'red check',
50
+ gates: 'checks',
51
+ review: 'ticket review',
52
+ } as const
53
+
54
+ /**
55
+ * A fixer's start in a review fix round. The round is the review's round (a
56
+ * round follows each review that asked for changes); only the round's first
57
+ * fixer adds its "tried" line. The card is fixing, at its step.
58
+ */
59
+ const reviewFixStarted = ({
60
+ card,
61
+ role,
62
+ }: {
63
+ card: TicketCard
64
+ role: string
65
+ }): TicketCard => {
66
+ const round = Math.max(card.review_round, 1)
67
+ const counts = card.findings
68
+ ? reviewCountsText({ findings: card.findings, nits: false })
69
+ : null
70
+ return {
71
+ ...card,
72
+ started: true,
73
+ role,
74
+ stage: 'fixing',
75
+ review_fix_round: round,
76
+ activity: `fixing the review's findings (${round}/${LOOP_CAP})`,
77
+ tried:
78
+ card.review_fix_round < round
79
+ ? withTried({
80
+ tried: card.tried,
81
+ line: `Review fix round ${round}/${LOOP_CAP}${counts ? `: ${counts}` : ''}`,
82
+ })
83
+ : card.tried,
84
+ }
85
+ }
86
+
87
+ /** The card's stage, step and activity once `role` starts on it. */
88
+ const startedCard = ({
89
+ card,
90
+ role,
91
+ follow_up_of,
92
+ }: {
93
+ card: TicketCard
94
+ role: string
95
+ follow_up_of: string | null
96
+ }): TicketCard => {
97
+ const kind = startKind({ card, role, follow_up_of })
98
+ if (
99
+ kind === 'review_fix' ||
100
+ (kind === 'resent' && card.open_check === 'review')
101
+ ) {
102
+ return reviewFixStarted({ card, role })
103
+ }
104
+ const fix_round = kind === 'fix' ? card.fix_round + 1 : card.fix_round
105
+ const started = { ...card, started: true, role, fix_round }
106
+ const tried =
107
+ kind === 'fix' && card.open_check !== null
108
+ ? withTried({
109
+ tried: card.tried,
110
+ line: `Fix round ${fix_round}/${LOOP_CAP} after the ${OPEN_CHECK_TEXT[card.open_check]}`,
111
+ })
112
+ : card.tried
113
+ const fixing = kind === 'fix' || kind === 'resent'
114
+ // A writer answering an open check, or fixing on top of the run
115
+ // branch, is fixing; its first pass is building.
116
+ const stage =
117
+ card.open_check !== null || card.stage === 'fixing'
118
+ ? 'fixing'
119
+ : 'building'
120
+ if (role === 'test-writer') {
121
+ // A retried review fixer stays at its step.
122
+ return {
123
+ ...started,
124
+ tried,
125
+ stage,
126
+ step: card.open_check === 'review' ? card.step : 0,
127
+ activity: fixing
128
+ ? `fixing tests (${fix_round}/${LOOP_CAP})`
129
+ : kind === 'retry'
130
+ ? 'writing tests again'
131
+ : 'writing tests',
132
+ }
133
+ }
134
+ if (role === 'implementer') {
135
+ return {
136
+ ...started,
137
+ tried,
138
+ stage,
139
+ step: 2,
140
+ activity: fixing
141
+ ? `fixing (${fix_round}/${LOOP_CAP})`
142
+ : kind === 'retry'
143
+ ? 'coding again'
144
+ : 'coding',
145
+ }
146
+ }
147
+ if (role === 'ticket-reviewer') {
148
+ const retry = kind === 'retry'
149
+ return {
150
+ ...started,
151
+ stage: 'reviewing',
152
+ step: 4,
153
+ open_check: retry ? card.open_check : null,
154
+ review_round: retry
155
+ ? Math.max(card.review_round, 1)
156
+ : card.review_round + 1,
157
+ activity: retry
158
+ ? 'reviewing again'
159
+ : card.review_fix_round > 0
160
+ ? 're-reviewing'
161
+ : 'reviewing',
162
+ }
163
+ }
164
+ return { ...started, activity: role }
165
+ }
166
+
167
+ /**
168
+ * The card's model (#630): the agent's model and why, with its role's
169
+ * waiting `agent_model` answer, which it uses up. A follow-up asks Clef
170
+ * nothing new, so it keeps the role's last used answer. No model when the
171
+ * journal names none.
172
+ */
173
+ const withModel = ({
174
+ card,
175
+ role,
176
+ follow_up_of,
177
+ model,
178
+ model_reason,
179
+ }: {
180
+ card: TicketCard
181
+ role: string
182
+ follow_up_of: string | null
183
+ model: string | null
184
+ model_reason: string | null
185
+ }): TicketCard => {
186
+ const waiting = card.model_answer?.role === role ? card.model_answer : null
187
+ const answer =
188
+ waiting ??
189
+ (follow_up_of === null ? null : (card.model_answers[role] ?? null))
190
+ return {
191
+ ...card,
192
+ model:
193
+ model === null
194
+ ? null
195
+ : { role, name: model, reason: model_reason, answer },
196
+ model_answer: waiting === null ? card.model_answer : null,
197
+ model_answers:
198
+ waiting === null
199
+ ? card.model_answers
200
+ : { ...card.model_answers, [role]: waiting },
201
+ }
202
+ }
203
+
204
+ export const agentStarted = ({
205
+ state,
206
+ record,
207
+ }: {
208
+ state: BoardState
209
+ record: Extract<BoardRecord, { kind: 'agent_started' }>
210
+ }): BoardState => {
211
+ const { role, follow_up_of, model, model_reason } = record.content
212
+ return updateTicket({
213
+ state,
214
+ number: record.ticket,
215
+ update: (card) =>
216
+ withModel({
217
+ card: startedCard({ card, role, follow_up_of }),
218
+ role,
219
+ follow_up_of,
220
+ model,
221
+ model_reason,
222
+ }),
223
+ })
224
+ }