@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,160 @@
1
+ /**
2
+ * An agent's turn on a ticket or the final review: its start, how it
3
+ * finished, and how it failed.
4
+ */
5
+ import type { BoardRecord } from './board-vocabulary'
6
+ import { agentStarted } from './reduce-board-agent-start'
7
+ import { countFindings, failureText, roleWords } from './reduce-board-text'
8
+ import {
9
+ LEARNER,
10
+ finalTried,
11
+ updateTicket,
12
+ withLearner,
13
+ withTried,
14
+ } from './reduce-board-update'
15
+
16
+ import { ALL_STEPS_DONE, type BoardState } from '../shared/board-state'
17
+
18
+ const agentFinished = ({
19
+ state,
20
+ record,
21
+ }: {
22
+ state: BoardState
23
+ record: Extract<BoardRecord, { kind: 'agent_finished' }>
24
+ }): BoardState => {
25
+ const { role, result } = record.content
26
+ return updateTicket({
27
+ state,
28
+ number: record.ticket,
29
+ update: (card) => {
30
+ const wontFix = result.finding_responses.filter(
31
+ ({ response }) => response === 'wont_fix'
32
+ )
33
+ const done = {
34
+ ...card,
35
+ role: null,
36
+ failed_turn: null,
37
+ tried: wontFix.reduce(
38
+ (tried, { finding_id, reason }) =>
39
+ withTried({
40
+ tried,
41
+ line: `Won't fix ${finding_id}: ${reason}`,
42
+ }),
43
+ card.tried
44
+ ),
45
+ }
46
+ if (role === 'test-writer') {
47
+ if (card.open_check === 'review') {
48
+ return { ...done, activity: 'test findings answered' }
49
+ }
50
+ if (result.outcome === 'already_done') {
51
+ return { ...done, activity: 'already done' }
52
+ }
53
+ return result.outcome === 'nothing_new_to_test'
54
+ ? { ...done, step: 2, activity: 'nothing new to test' }
55
+ : { ...done, step: 1, activity: 'red check' }
56
+ }
57
+ if (role === 'implementer') {
58
+ return result.outcome === 'bad_test'
59
+ ? { ...done, activity: 'bad test' }
60
+ : { ...done, step: 3, activity: 'checks' }
61
+ }
62
+ if (role === 'ticket-reviewer') {
63
+ const findings = result.findings
64
+ ? countFindings({ findings: result.findings })
65
+ : card.findings
66
+ const ruled = result.rulings.reduce(
67
+ (tried, { finding_id, ruling, reason }) =>
68
+ withTried({
69
+ tried,
70
+ line:
71
+ ruling === 'accepted'
72
+ ? `Declined ${finding_id} accepted: ${reason}`
73
+ : `${finding_id} still stands: ${reason}`,
74
+ }),
75
+ done.tried
76
+ )
77
+ // Approved, it joins the run branch next; else a review fix
78
+ // round follows.
79
+ return result.verdict === 'approve'
80
+ ? {
81
+ ...done,
82
+ stage: 'joining',
83
+ step: ALL_STEPS_DONE,
84
+ findings,
85
+ open_check: null,
86
+ tried: ruled,
87
+ activity: 'approved',
88
+ }
89
+ : {
90
+ ...done,
91
+ stage: 'fixing',
92
+ findings,
93
+ open_check: 'review',
94
+ tried: ruled,
95
+ activity: 'changes requested',
96
+ }
97
+ }
98
+ return done
99
+ },
100
+ })
101
+ }
102
+
103
+ /** The record kinds of an agent's turn. */
104
+ export const AGENT_KINDS = [
105
+ 'agent_started',
106
+ 'agent_finished',
107
+ 'agent_failed',
108
+ ] as const
109
+
110
+ type AgentRecord = Extract<BoardRecord, { kind: (typeof AGENT_KINDS)[number] }>
111
+
112
+ /** Applies one record of AGENT_KINDS to the board. */
113
+ export const applyAgentKind = ({
114
+ state,
115
+ record,
116
+ }: {
117
+ state: BoardState
118
+ record: AgentRecord
119
+ }): BoardState => {
120
+ const { ticket } = record
121
+ switch (record.kind) {
122
+ case 'agent_started':
123
+ // The learner (#370) has no ticket and is no final review agent.
124
+ if (record.content.role === LEARNER) {
125
+ return withLearner({ state, learner: 'learning' })
126
+ }
127
+ return agentStarted({ state, record })
128
+ case 'agent_finished':
129
+ if (record.content.role === LEARNER) {
130
+ return withLearner({ state, learner: 'learned' })
131
+ }
132
+ return agentFinished({ state, record })
133
+ case 'agent_failed': {
134
+ const { role, error, failure } = record.content
135
+ // A failed learner gets a fresh one; its row says so.
136
+ if (role === LEARNER) return state
137
+ // A final review agent (a lens or a fixer) has no ticket.
138
+ if (ticket === null) {
139
+ return finalTried({
140
+ state,
141
+ line: `The ${roleWords({ role })} ${failureText({ failure })}: ${error}`,
142
+ })
143
+ }
144
+ return updateTicket({
145
+ state,
146
+ number: ticket,
147
+ update: (card) => ({
148
+ ...card,
149
+ role: null,
150
+ failed_turn: role,
151
+ activity: `${role} ${failure === 'engine' ? 'could not run' : 'failed'}`,
152
+ tried: withTried({
153
+ tried: card.tried,
154
+ line: `The ${role} ${failureText({ failure })}: ${error}`,
155
+ }),
156
+ }),
157
+ })
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The run-wide counters: Jev's questions, agent messages, changes to the
3
+ * shared git folder, and what the memory searched, saved and learned.
4
+ */
5
+ import type { BoardRecord } from './board-vocabulary'
6
+ import { updateTicket, withLearner } from './reduce-board-update'
7
+
8
+ import type { BoardState } from '../shared/board-state'
9
+
10
+ /** The record kinds of the run-wide counters. */
11
+ export const COUNTER_KINDS = [
12
+ 'jev_asked',
13
+ 'jev_answered',
14
+ 'jev_failed',
15
+ 'decision_model_off',
16
+ 'agent_message',
17
+ 'agent_message_delivered',
18
+ 'shared_git_changed',
19
+ 'memory_recalled',
20
+ 'memories_saved',
21
+ 'learning_skipped',
22
+ 'memories_reported',
23
+ ] as const
24
+
25
+ type CounterRecord = Extract<
26
+ BoardRecord,
27
+ { kind: (typeof COUNTER_KINDS)[number] }
28
+ >
29
+
30
+ /** Applies one record of COUNTER_KINDS to the board. */
31
+ export const applyCounterKind = ({
32
+ state,
33
+ record,
34
+ }: {
35
+ state: BoardState
36
+ record: CounterRecord
37
+ }): BoardState => {
38
+ switch (record.kind) {
39
+ case 'jev_asked':
40
+ return {
41
+ ...state,
42
+ jev: { ...state.jev, asked: state.jev.asked + 1 },
43
+ }
44
+ case 'jev_answered': {
45
+ const counted = {
46
+ ...state,
47
+ jev: { ...state.jev, answered: state.jev.answered + 1 },
48
+ }
49
+ const { job, answers } = record.content
50
+ const { role } = record
51
+ // An agent's model pick waits on its card for its `agent_started`.
52
+ if (job !== 'agent_model' || role === null) return counted
53
+ const pick = answers.model?.value ?? null
54
+ return updateTicket({
55
+ state: counted,
56
+ number: record.ticket,
57
+ update: (card) => ({
58
+ ...card,
59
+ model_answer: {
60
+ role,
61
+ pick: pick === null ? null : String(pick),
62
+ confidence: answers.model?.confidence ?? null,
63
+ },
64
+ }),
65
+ })
66
+ }
67
+ case 'jev_failed':
68
+ return {
69
+ ...state,
70
+ jev: { ...state.jev, failed: state.jev.failed + 1 },
71
+ }
72
+ // The decision model turning off is only a chat line (#534).
73
+ case 'decision_model_off':
74
+ return state
75
+ case 'agent_message': {
76
+ const refused = record.content.status === 'refused'
77
+ return {
78
+ ...state,
79
+ messages: {
80
+ ...state.messages,
81
+ sent: state.messages.sent + (refused ? 0 : 1),
82
+ refused: state.messages.refused + (refused ? 1 : 0),
83
+ },
84
+ }
85
+ }
86
+ case 'agent_message_delivered':
87
+ return {
88
+ ...state,
89
+ messages: {
90
+ ...state.messages,
91
+ delivered:
92
+ state.messages.delivered + record.content.ids.length,
93
+ },
94
+ }
95
+ // Another process's change to the shared `.git`: only counted.
96
+ case 'shared_git_changed':
97
+ return {
98
+ ...state,
99
+ shared_git_changed: state.shared_git_changed + 1,
100
+ }
101
+ case 'memory_recalled': {
102
+ const { vaults, memories } = record.content
103
+ const { memory } = state
104
+ return {
105
+ ...state,
106
+ memory: {
107
+ ...memory,
108
+ searches: memory.searches + 1,
109
+ search_errors:
110
+ memory.search_errors +
111
+ vaults.filter(({ ok }) => !ok).length,
112
+ shown: memory.shown + memories.length,
113
+ },
114
+ }
115
+ }
116
+ case 'memories_saved': {
117
+ const count = (outcome: string) =>
118
+ record.content.saves.filter((save) => save.outcome === outcome)
119
+ .length
120
+ const { memory } = state
121
+ return {
122
+ ...state,
123
+ memory: {
124
+ ...memory,
125
+ added: memory.added + count('added'),
126
+ updated: memory.updated + count('updated'),
127
+ refused: memory.refused + count('refused'),
128
+ failed: memory.failed + count('failed'),
129
+ },
130
+ }
131
+ }
132
+ case 'learning_skipped':
133
+ return withLearner({ state, learner: 'skipped' })
134
+ // The listing on the spec issue is only a chat row.
135
+ case 'memories_reported':
136
+ return state
137
+ }
138
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * A new board's starting shapes: the board before any record, and a ticket's
3
+ * card when its snapshot first arrives.
4
+ */
5
+ import {
6
+ DEFAULT_CONFIDENCE_BAR,
7
+ DEFAULT_RUN_BUDGET_TOKENS,
8
+ LENS_NAMES,
9
+ NO_MEMORY,
10
+ USAGE_LABEL,
11
+ type BoardState,
12
+ type FindingCounts,
13
+ type LensCard,
14
+ type TicketCard,
15
+ } from '../shared/board-state'
16
+
17
+ const noFindings = (): FindingCounts => ({ blocker: 0, should_fix: 0, nit: 0 })
18
+
19
+ const freshLenses = (): LensCard[] =>
20
+ LENS_NAMES.map((name) => ({
21
+ name,
22
+ state: 'waiting',
23
+ findings: noFindings(),
24
+ }))
25
+
26
+ /**
27
+ * A new run's board, before any record.
28
+ *
29
+ * @example
30
+ * const state = createBoardState({
31
+ * run_id: 'luca-20260923-123042-ab12',
32
+ * spec_number: 10,
33
+ * demo: false,
34
+ * started_at: '2026-09-23T12:30:42.000Z',
35
+ * log_path: '/tmp/luca-20260923-123042-ab12.log',
36
+ * })
37
+ */
38
+ export const createBoardState = ({
39
+ run_id,
40
+ spec_number,
41
+ demo,
42
+ started_at,
43
+ log_path,
44
+ }: {
45
+ run_id: string
46
+ spec_number: number | null
47
+ demo: boolean
48
+ started_at: string
49
+ log_path: string | null
50
+ }): BoardState => ({
51
+ run: {
52
+ run_id,
53
+ spec_number,
54
+ spec_title: null,
55
+ demo,
56
+ branch: null,
57
+ phase: 'starting',
58
+ status: 'starting',
59
+ pr_url: null,
60
+ pr_number: null,
61
+ refusal: [],
62
+ started_at,
63
+ last_time: null,
64
+ engine_ended: null,
65
+ stopped: null,
66
+ log_path,
67
+ started_on: null,
68
+ version_note: null,
69
+ spec_author: null,
70
+ spec_repo: null,
71
+ },
72
+ for_a_person: [],
73
+ usage: null,
74
+ usage_label: USAGE_LABEL,
75
+ run_tokens: 0,
76
+ run_budget_tokens: DEFAULT_RUN_BUDGET_TOKENS,
77
+ run_budget_each: DEFAULT_RUN_BUDGET_TOKENS,
78
+ run_stuck_reason: null,
79
+ confidence_bar: DEFAULT_CONFIDENCE_BAR,
80
+ limit_wait: null,
81
+ usage_line_wait: null,
82
+ repo_wait: null,
83
+ run_plan_used: [],
84
+ needs_you: [],
85
+ tickets: [],
86
+ final_review: {
87
+ state: 'waiting',
88
+ active: false,
89
+ round: 0,
90
+ fix_round: 0,
91
+ lenses: freshLenses(),
92
+ tried: [],
93
+ },
94
+ jev: { asked: 0, answered: 0, failed: 0 },
95
+ messages: { sent: 0, refused: 0, delivered: 0 },
96
+ memory: NO_MEMORY,
97
+ shared_git_changed: 0,
98
+ run_steps: [],
99
+ live_agents: [],
100
+ current_step: null,
101
+ feed: [],
102
+ live_written_at: null,
103
+ engine_gone: false,
104
+ event_count: 0,
105
+ latest: null,
106
+ latest_at: null,
107
+ })
108
+
109
+ export const newTicket = ({
110
+ number,
111
+ title,
112
+ refactor,
113
+ blockers,
114
+ }: {
115
+ number: number
116
+ title: string
117
+ refactor: boolean
118
+ blockers: number[]
119
+ }): TicketCard => ({
120
+ number,
121
+ title,
122
+ refactor,
123
+ blockers,
124
+ started: false,
125
+ stage: 'building',
126
+ step: -1,
127
+ activity: 'queued',
128
+ role: null,
129
+ model: null,
130
+ model_answer: null,
131
+ model_answers: {},
132
+ current_step: null,
133
+ wait: null,
134
+ fix_round: 0,
135
+ review_round: 0,
136
+ review_fix_round: 0,
137
+ open_check: null,
138
+ failed_turn: null,
139
+ tests: null,
140
+ baseline_tests: null,
141
+ findings: null,
142
+ tokens: 0,
143
+ agent_tokens: {},
144
+ tried: [],
145
+ plan_used: [],
146
+ live: null,
147
+ slot_wait: null,
148
+ })
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The final review: its rounds, its lenses, the fix rounds, and how it ends
3
+ * (passed, shipped by a person, or stuck).
4
+ */
5
+ import type { BoardRecord } from './board-vocabulary'
6
+ import { finalReasonText } from './reduce-board-reasons'
7
+ import { clip } from './reduce-board-text'
8
+ import {
9
+ DETAIL_MAX,
10
+ addNeedsYou,
11
+ resolveNeedsYou,
12
+ updateFinal,
13
+ withTried,
14
+ } from './reduce-board-update'
15
+
16
+ import { LOOP_CAP, type BoardState, type LensCard } from '../shared/board-state'
17
+ import { FINAL_KEY, allowedReplies } from '../shared/reply-actions'
18
+
19
+ const updateLens = ({
20
+ state,
21
+ lens,
22
+ update,
23
+ }: {
24
+ state: BoardState
25
+ lens: string
26
+ update: (card: LensCard) => LensCard
27
+ }): BoardState =>
28
+ updateFinal({
29
+ state,
30
+ update: (review) => ({
31
+ ...review,
32
+ lenses: review.lenses.map((card) =>
33
+ card.name === lens ? update(card) : card
34
+ ),
35
+ }),
36
+ })
37
+
38
+ /** The record kinds of the final review. */
39
+ export const FINAL_REVIEW_KINDS = [
40
+ 'final_review_started',
41
+ 'lens_started',
42
+ 'lens_finished',
43
+ 'final_review_fixing',
44
+ 'final_review_stuck',
45
+ 'final_review_shipped',
46
+ 'final_review_passed',
47
+ ] as const
48
+
49
+ type FinalReviewRecord = Extract<
50
+ BoardRecord,
51
+ { kind: (typeof FINAL_REVIEW_KINDS)[number] }
52
+ >
53
+
54
+ /** Applies one record of FINAL_REVIEW_KINDS to the board. */
55
+ export const applyFinalReviewKind = ({
56
+ state,
57
+ record,
58
+ }: {
59
+ state: BoardState
60
+ record: FinalReviewRecord
61
+ }): BoardState => {
62
+ switch (record.kind) {
63
+ case 'final_review_started':
64
+ return {
65
+ ...updateFinal({
66
+ state,
67
+ update: (review) => ({
68
+ ...review,
69
+ state: 'reviewing',
70
+ round: review.round + 1,
71
+ lenses: review.lenses.map((lens) =>
72
+ lens.state === 'clean'
73
+ ? lens
74
+ : { ...lens, state: 'waiting' }
75
+ ),
76
+ }),
77
+ }),
78
+ run: { ...state.run, phase: 'final_review' },
79
+ }
80
+ case 'lens_started':
81
+ return updateLens({
82
+ state,
83
+ lens: record.content.lens,
84
+ update: (lens) => ({ ...lens, state: 'reviewing' }),
85
+ })
86
+ case 'lens_finished': {
87
+ const { findings } = record.content
88
+ const counts = {
89
+ blocker: findings.blocker,
90
+ should_fix: findings.should_fix,
91
+ nit: findings.nit,
92
+ }
93
+ return updateLens({
94
+ state,
95
+ lens: record.content.lens,
96
+ update: (lens) => ({
97
+ ...lens,
98
+ findings: counts,
99
+ state:
100
+ counts.blocker + counts.should_fix > 0
101
+ ? 'fixing'
102
+ : 'clean',
103
+ }),
104
+ })
105
+ }
106
+ case 'final_review_fixing':
107
+ return updateFinal({
108
+ state,
109
+ update: (review) => ({
110
+ ...review,
111
+ state: 'fixing',
112
+ fix_round: record.content.round,
113
+ tried: withTried({
114
+ tried: review.tried,
115
+ // Every lens finished clean: only failed slow tests
116
+ // open a fix round then (#563).
117
+ line: `Fix round ${record.content.round}/${LOOP_CAP} on ${
118
+ review.lenses.length > 0 &&
119
+ review.lenses.every(
120
+ (lens) => lens.state === 'clean'
121
+ )
122
+ ? 'the failed slow tests'
123
+ : "the lenses' findings"
124
+ }`,
125
+ }),
126
+ }),
127
+ })
128
+ case 'final_review_stuck':
129
+ return addNeedsYou({
130
+ state: updateFinal({
131
+ state,
132
+ update: (review) => ({ ...review, state: 'stuck' }),
133
+ }),
134
+ item: {
135
+ key: FINAL_KEY,
136
+ ticket: null,
137
+ subject: 'The final review is stuck',
138
+ reason: finalReasonText({ reason: record.content.reason }),
139
+ detail: clip({
140
+ text: record.content.detail,
141
+ max: DETAIL_MAX,
142
+ }),
143
+ tried: state.final_review.tried,
144
+ replies: allowedReplies({ key: FINAL_KEY, ticket: null }),
145
+ since: record.time,
146
+ },
147
+ })
148
+ // A `ship` reply to the stuck final review, as the engine journals
149
+ // it: the same as the reply itself.
150
+ case 'final_review_shipped':
151
+ return resolveNeedsYou({
152
+ state: updateFinal({
153
+ state,
154
+ update: (review) => ({ ...review, state: 'passed' }),
155
+ }),
156
+ key: FINAL_KEY,
157
+ })
158
+ case 'final_review_passed':
159
+ return resolveNeedsYou({
160
+ state: updateFinal({
161
+ state,
162
+ update: (review) => ({
163
+ ...review,
164
+ state: 'passed',
165
+ lenses: review.lenses.map((lens) => ({
166
+ ...lens,
167
+ state: 'clean',
168
+ })),
169
+ }),
170
+ }),
171
+ key: FINAL_KEY,
172
+ })
173
+ }
174
+ }