@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,203 @@
1
+ /**
2
+ * Small pure reads of an agent turn, shared by ticket and final review
3
+ * replay: a finished agent's assumptions and bad test, the sessions after a
4
+ * turn, the out-of-turns counts, the fixer roles, a fix round's answers and `other_role` hand-offs,
5
+ * and the "won't fix" answers a review let go.
6
+ */
7
+ import omit from 'lodash/omit'
8
+ import uniq from 'lodash/uniq'
9
+
10
+ import type { JournalRecord } from './journal-record'
11
+ import type { DeclinedFinding, ReviewFix, WriterRole } from './ticket-progress'
12
+
13
+ import type {
14
+ AgentRole,
15
+ BadTest,
16
+ FindingResponse,
17
+ ImplementerResult,
18
+ TicketReviewResult,
19
+ } from '../agents/role-results'
20
+ import { DEFAULT_MODEL } from '../config/engine-config'
21
+
22
+ export type FinishedContent = Extract<
23
+ JournalRecord,
24
+ { kind: 'agent_finished' }
25
+ >['content']
26
+
27
+ /** A finished agent's assumptions; the learner makes none. */
28
+ export const assumptionsOf = (finished: FinishedContent): string[] =>
29
+ 'assumptions' in finished.result ? finished.result.assumptions : []
30
+
31
+ /** A fixer's bad test, or the implementer's summary as its reason. */
32
+ export const badTestOf = (result: ImplementerResult): BadTest | null =>
33
+ result.outcome === 'bad_test'
34
+ ? (result.bad_test ?? { file: '', name: '', reason: result.summary })
35
+ : null
36
+
37
+ /** The sessions after a turn: the role's latest, or none if unknown. */
38
+ export const sessionsAfter = ({
39
+ sessions,
40
+ role,
41
+ session_id,
42
+ }: {
43
+ sessions: Partial<Record<AgentRole, string>>
44
+ role: AgentRole
45
+ session_id: string | null
46
+ }): Partial<Record<AgentRole, string>> => {
47
+ const rest = omit(sessions, role)
48
+ return session_id === null ? rest : { ...rest, [role]: session_id }
49
+ }
50
+
51
+ /** The out-of-turns counts after a failed turn (#543): one more for its role if it reached its limit. */
52
+ export const outOfTurnsAfter = ({
53
+ counts,
54
+ failed,
55
+ }: {
56
+ counts: Partial<Record<AgentRole, number>>
57
+ failed: { role: AgentRole; out_of_turns: boolean }
58
+ }): Partial<Record<AgentRole, number>> =>
59
+ failed.out_of_turns
60
+ ? { ...counts, [failed.role]: (counts[failed.role] ?? 0) + 1 }
61
+ : counts
62
+
63
+ export const isFixerRole = (role: AgentRole): boolean =>
64
+ role === 'test-writer' || role === 'implementer'
65
+
66
+ const OTHER_ROLE: Record<WriterRole, WriterRole> = {
67
+ 'test-writer': 'implementer',
68
+ implementer: 'test-writer',
69
+ }
70
+
71
+ type FixAnswers = Pick<ReviewFix, 'findings' | 'responses' | 'handed_off'>
72
+
73
+ /**
74
+ * A fixer's answers in its fix round, kept, with the round's findings it
75
+ * answered `other_role` handed to the other writer role (#616).
76
+ */
77
+ export const fixerAnswered = ({
78
+ fix,
79
+ role,
80
+ responses,
81
+ }: {
82
+ fix: FixAnswers
83
+ role: WriterRole
84
+ responses: FindingResponse[]
85
+ }): Pick<ReviewFix, 'responses' | 'handed_off'> => {
86
+ const handed = responses.flatMap(({ finding_id, response }) =>
87
+ response === 'other_role' &&
88
+ fix.findings.some(({ id }) => id === finding_id)
89
+ ? [finding_id]
90
+ : []
91
+ )
92
+ const other = OTHER_ROLE[role]
93
+ return {
94
+ responses: [...fix.responses, ...responses],
95
+ handed_off:
96
+ handed.length === 0
97
+ ? fix.handed_off
98
+ : {
99
+ ...fix.handed_off,
100
+ [other]: uniq([
101
+ ...(fix.handed_off[other] ?? []),
102
+ ...handed,
103
+ ]),
104
+ },
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The other role's answers to the findings handed to it (#616). Each one
110
+ * replaces the first fixer's `other_role`, and is final: a second
111
+ * `other_role` counts as `wont_fix`. Answers to findings not handed to it
112
+ * are dropped.
113
+ */
114
+ export const handoffAnswered = ({
115
+ fix,
116
+ role,
117
+ responses,
118
+ }: {
119
+ fix: FixAnswers
120
+ role: WriterRole
121
+ responses: FindingResponse[]
122
+ }): Pick<ReviewFix, 'responses' | 'handed_off'> => {
123
+ const ids = fix.handed_off[role] ?? []
124
+ const answers = responses.flatMap((answer) => {
125
+ if (!ids.includes(answer.finding_id)) return []
126
+ return answer.response === 'other_role'
127
+ ? [{ ...answer, response: 'wont_fix' as const }]
128
+ : [answer]
129
+ })
130
+ const answered = new Set(answers.map(({ finding_id }) => finding_id))
131
+ return {
132
+ responses: [
133
+ ...fix.responses.filter(
134
+ ({ finding_id }) => !answered.has(finding_id)
135
+ ),
136
+ ...answers,
137
+ ],
138
+ handed_off: omit(fix.handed_off, role),
139
+ }
140
+ }
141
+
142
+ /**
143
+ * The writer role a fix round hands findings to next, once its fixers
144
+ * answered: the test-writer first, then the implementer. `null` with none,
145
+ * or while a fixer has yet to answer.
146
+ */
147
+ export const handoffRole = ({
148
+ fix,
149
+ }: {
150
+ fix: Pick<ReviewFix, 'handed_off' | 'tests_answered' | 'code_answered'>
151
+ }): WriterRole | null => {
152
+ if (!fix.tests_answered || !fix.code_answered) return null
153
+ return (
154
+ (['test-writer', 'implementer'] as const).find(
155
+ (role) => (fix.handed_off[role] ?? []).length > 0
156
+ ) ?? null
157
+ )
158
+ }
159
+
160
+ /**
161
+ * Whether a fixer's answer declines its finding: `wont_fix`, or an
162
+ * `other_role` the other role never answered (#616).
163
+ */
164
+ export const isDeclined = ({ response }: FindingResponse): boolean =>
165
+ response === 'wont_fix' || response === 'other_role'
166
+
167
+ /**
168
+ * Whether an agent launched on a model cheaper than Opus (#604). Older
169
+ * journals name no model, and ran Opus.
170
+ */
171
+ export const ranCheaper = (model: string | null | undefined): boolean =>
172
+ model !== null && model !== undefined && model !== DEFAULT_MODEL
173
+
174
+ /**
175
+ * The fixers' "won't fix" answers the new review let go: its ruling was
176
+ * `accepted`, or it gave none and did not list the finding again.
177
+ */
178
+ export const declinedBy = ({
179
+ fix,
180
+ review,
181
+ }: {
182
+ fix: Pick<ReviewFix, 'findings' | 'responses'> | null
183
+ review: TicketReviewResult
184
+ }): DeclinedFinding[] => {
185
+ if (fix === null) return []
186
+ const listed = new Set(review.findings.map(({ id }) => id))
187
+ return fix.responses.flatMap((answer) => {
188
+ const { finding_id, reason } = answer
189
+ if (!isDeclined(answer)) return []
190
+ const finding = fix.findings.find(({ id }) => id === finding_id)
191
+ if (finding === undefined) return []
192
+ const ruling = review.rulings.find(
193
+ (entry) => entry.finding_id === finding_id
194
+ )
195
+ const accepted =
196
+ ruling === undefined
197
+ ? !listed.has(finding_id)
198
+ : ruling.ruling === 'accepted'
199
+ return accepted
200
+ ? [{ finding, reason, ruling: ruling?.reason ?? '' }]
201
+ : []
202
+ })
203
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod'
2
+
3
+ import { ENTRY_FIELDS } from './entry-fields'
4
+
5
+ import { TestRunSchema } from '../gates/gate-schemas'
6
+
7
+ /**
8
+ * The journal entries of a ticket whose work is already on the base branch
9
+ * (#484): the evidence check, the ticket counted done, and its closing.
10
+ */
11
+
12
+ /**
13
+ * The engine checked a test-writer's `already_done` evidence (#495) in the
14
+ * ticket's worktree, at `base_sha`, the commit it started from: each named
15
+ * commit is on the base, and each named test exists and passes there.
16
+ * `problems` says what did not check out; `tests` is the run of the named
17
+ * test files (`null` if none ran). A failed check is a failed try for the
18
+ * test-writer.
19
+ */
20
+ export const AlreadyDoneCheckedEntrySchema = z.object({
21
+ ...ENTRY_FIELDS,
22
+ kind: z.literal('already_done_checked'),
23
+ content: z.object({
24
+ ok: z.boolean(),
25
+ problems: z.array(z.string()),
26
+ base_sha: z.string(),
27
+ tests: TestRunSchema.nullable(),
28
+ }),
29
+ })
30
+
31
+ /**
32
+ * A ticket whose work is already on the base branch (#484): its
33
+ * test-writer answered `already_done`, and `shas` are the commits that did
34
+ * the work. The ticket counts as done, not stuck, and tickets that wait on
35
+ * it go on. The engine said so on the spec issue, in its comment
36
+ * `comment_id`. The run's PR closes it; with no PR, `ticket_closed` follows.
37
+ */
38
+ export const TicketAlreadyDoneEntrySchema = z.object({
39
+ ...ENTRY_FIELDS,
40
+ kind: z.literal('ticket_already_done'),
41
+ content: z.object({
42
+ shas: z.array(z.string().min(1)),
43
+ comment_id: z.number().int(),
44
+ }),
45
+ })
46
+
47
+ /**
48
+ * The engine closed an already-done ticket itself, as the run opens no PR
49
+ * to close it, after its comment `comment_id` on the ticket said why.
50
+ */
51
+ export const TicketClosedEntrySchema = z.object({
52
+ ...ENTRY_FIELDS,
53
+ kind: z.literal('ticket_closed'),
54
+ content: z.object({ comment_id: z.number().int() }),
55
+ })
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod'
2
+
3
+ import { ENTRY_FIELDS } from './entry-fields'
4
+
5
+ import { LeftoverHitSchema, LeftoverPassSchema } from '../gates/gate-schemas'
6
+
7
+ /**
8
+ * The journal entries of the engine's commits on a ticket: the leftover
9
+ * scan, dropped changesets, and the commit itself.
10
+ */
11
+
12
+ /**
13
+ * The commits the engine makes on a ticket: tests first (`red`), then code
14
+ * (`green`), then one per review fix round (`fix`).
15
+ */
16
+ export const CommitStageSchema = z.enum(['red', 'green', 'fix'])
17
+
18
+ export type CommitStage = z.infer<typeof CommitStageSchema>
19
+
20
+ /**
21
+ * The leftover scan before an engine commit. Any hit blocks the commit.
22
+ * `passed` lists the new markdown it let through and why (#520); left out
23
+ * when there was none, and in journals from before it.
24
+ */
25
+ export const LeftoverScanEntrySchema = z.object({
26
+ ...ENTRY_FIELDS,
27
+ kind: z.literal('leftover_scan'),
28
+ content: z.object({
29
+ stage: CommitStageSchema,
30
+ hits: z.array(LeftoverHitSchema),
31
+ passed: z.array(LeftoverPassSchema).optional(),
32
+ }),
33
+ })
34
+
35
+ /**
36
+ * Before an engine commit's leftover scan, in a repo with changesets, the
37
+ * engine removed the changesets an agent wrote (new `.changeset/*.md` files
38
+ * other than the README) from the worktree: it writes the run's one itself.
39
+ * No ticket for the final review's fix commit.
40
+ */
41
+ export const ChangesetDroppedEntrySchema = z.object({
42
+ ...ENTRY_FIELDS,
43
+ kind: z.literal('changeset_dropped'),
44
+ content: z.object({ paths: z.array(z.string().min(1)).min(1) }),
45
+ })
46
+
47
+ export const CommitMadeEntrySchema = z.object({
48
+ ...ENTRY_FIELDS,
49
+ kind: z.literal('commit_made'),
50
+ content: z.object({
51
+ stage: CommitStageSchema,
52
+ sha: z.string().min(1),
53
+ message: z.string(),
54
+ files: z.array(z.string()),
55
+ }),
56
+ })
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Records that reach across tickets in replay: a restarted engine's
3
+ * `run_resumed`, a closed or gone session, and the other joins an undo took
4
+ * off the run branch.
5
+ */
6
+ import mapValues from 'lodash/mapValues'
7
+ import omit from 'lodash/omit'
8
+ import omitBy from 'lodash/omitBy'
9
+ import uniq from 'lodash/uniq'
10
+
11
+ import { isFixerRole } from './agent-turn-replay'
12
+ import type { JournalRecord } from './journal-record'
13
+ import type { RunState } from './run-state'
14
+ import { EMPTY_TICKET_PROGRESS } from './ticket-progress'
15
+
16
+ import { AgentRoleSchema, type AgentRole } from '../agents/role-results'
17
+
18
+ /** A failed turn's session forgotten, when it is this role's. */
19
+ const failureWithoutSession = <
20
+ Failure extends { role: AgentRole; session_id: string | null },
21
+ >({
22
+ failure,
23
+ role,
24
+ }: {
25
+ failure: Failure | null | undefined
26
+ role: AgentRole
27
+ }): Failure | null =>
28
+ failure === null || failure === undefined
29
+ ? null
30
+ : failure.role === role
31
+ ? { ...failure, session_id: null }
32
+ : failure
33
+
34
+ /**
35
+ * A restarted engine's `run_resumed`: each agent turn a crash cut off is
36
+ * taken again in a fresh session, so its role's session (and a failed
37
+ * turn's session, for a follow-up) is forgotten, on its ticket or in the
38
+ * final review, and the fresh agent is told of the crash. The counts it
39
+ * adds live in `crashes`.
40
+ */
41
+ export const resumedAfter = ({
42
+ state,
43
+ record,
44
+ }: {
45
+ state: RunState
46
+ record: Extract<JournalRecord, { kind: 'run_resumed' }>
47
+ }): RunState =>
48
+ record.content.interrupted.reduce((current, { key, ticket, role }) => {
49
+ const parsed = AgentRoleSchema.safeParse(role)
50
+ if (!parsed.success) return current
51
+ const agent = parsed.data
52
+ if (key === 'final') {
53
+ const review = current.final_review
54
+ const failed = failureWithoutSession({
55
+ failure: review.agent_failures[agent],
56
+ role: agent,
57
+ })
58
+ return {
59
+ ...current,
60
+ final_review: {
61
+ ...review,
62
+ sessions: omit(review.sessions, agent),
63
+ agent_failures:
64
+ failed === null
65
+ ? review.agent_failures
66
+ : { ...review.agent_failures, [agent]: failed },
67
+ crashed_turn: isFixerRole(agent)
68
+ ? agent
69
+ : review.crashed_turn,
70
+ },
71
+ }
72
+ }
73
+ if (ticket === null || key !== String(ticket)) return current
74
+ const progress = current.tickets[ticket] ?? EMPTY_TICKET_PROGRESS
75
+ return {
76
+ ...current,
77
+ tickets: {
78
+ ...current.tickets,
79
+ [ticket]: {
80
+ ...progress,
81
+ sessions: omit(progress.sessions, agent),
82
+ agent_failure: failureWithoutSession({
83
+ failure: progress.agent_failure,
84
+ role: agent,
85
+ }),
86
+ crashed_turn: agent,
87
+ },
88
+ },
89
+ }
90
+ }, state)
91
+
92
+ /**
93
+ * A closed session can take no follow-up: it is forgotten wherever the
94
+ * decision step would follow it up (a role's latest session, or a failed
95
+ * turn's), on its ticket, in the final review, or the learner's, so a fix
96
+ * round that would have gone to it goes to a fresh agent.
97
+ */
98
+ export const closedSessionAfter = ({
99
+ state,
100
+ session_id,
101
+ }: {
102
+ state: RunState
103
+ session_id: string
104
+ }): RunState => {
105
+ const forget = <Failure extends { session_id: string | null }>(
106
+ failure: Failure
107
+ ): Failure =>
108
+ failure.session_id === session_id
109
+ ? { ...failure, session_id: null }
110
+ : failure
111
+ const withoutIt = (sessions: Partial<Record<AgentRole, string>>) =>
112
+ omitBy(sessions, (id) => id === session_id)
113
+ const review = state.final_review
114
+ const { learner } = state.memory
115
+ return {
116
+ ...state,
117
+ tickets: mapValues(state.tickets, (progress) => ({
118
+ ...progress,
119
+ sessions: withoutIt(progress.sessions),
120
+ agent_failure:
121
+ progress.agent_failure === null
122
+ ? null
123
+ : forget(progress.agent_failure),
124
+ })),
125
+ final_review: {
126
+ ...review,
127
+ sessions: withoutIt(review.sessions),
128
+ agent_failures: mapValues(review.agent_failures, (failure) =>
129
+ failure === undefined ? failure : forget(failure)
130
+ ),
131
+ },
132
+ memory: {
133
+ ...state.memory,
134
+ learner: {
135
+ ...learner,
136
+ agent_failure:
137
+ learner.agent_failure === null
138
+ ? null
139
+ : forget(learner.agent_failure),
140
+ },
141
+ },
142
+ }
143
+ }
144
+
145
+ /**
146
+ * The session a follow-up found gone (#521), or `null`: no agent ran, so the
147
+ * record changes no step and counts no engine failure.
148
+ */
149
+ export const goneSessionOf = (record: JournalRecord): string | null =>
150
+ record.kind === 'agent_failed' && record.content.session_gone
151
+ ? record.content.session_id
152
+ : null
153
+
154
+ /**
155
+ * Closed sessions after a record: one journaled as closed, or found gone by
156
+ * a follow-up (#521), joins them and is forgotten; a later turn's end that
157
+ * names a closed session again (`agent_finished`, `agent_failed`) has it
158
+ * forgotten again, so it is never a follow-up target.
159
+ */
160
+ export const closedSessionsAfter = ({
161
+ state,
162
+ record,
163
+ }: {
164
+ state: RunState
165
+ record: JournalRecord
166
+ }): RunState => {
167
+ const closed =
168
+ record.kind === 'agent_session_closed'
169
+ ? record.content.session_id
170
+ : goneSessionOf(record)
171
+ if (closed !== null) {
172
+ return closedSessionAfter({
173
+ state: {
174
+ ...state,
175
+ closed_sessions: uniq([...state.closed_sessions, closed]),
176
+ },
177
+ session_id: closed,
178
+ })
179
+ }
180
+ if (record.kind !== 'agent_finished' && record.kind !== 'agent_failed') {
181
+ return state
182
+ }
183
+ const { session_id } = record.content
184
+ return session_id !== null && state.closed_sessions.includes(session_id)
185
+ ? closedSessionAfter({ state, session_id })
186
+ : state
187
+ }
188
+
189
+ /**
190
+ * The other tickets whose joins an undo took off the run branch (#519): an
191
+ * undo resets the run branch to before the undone ticket's join, so any
192
+ * join on top of it goes too. Each such ticket's join and join gates are
193
+ * cleared, as its own undo would, so it joins again on the run branch as it
194
+ * is now, and a stuck one is never undone a second time. Its push, stuck,
195
+ * and reply are kept.
196
+ */
197
+ export const undoneJoinsAfter = ({
198
+ state,
199
+ ticket,
200
+ shas,
201
+ }: {
202
+ state: RunState
203
+ /** The ticket whose join was undone. */
204
+ ticket: number | null
205
+ /** Every commit the undo took off the run branch. */
206
+ shas: string[]
207
+ }): RunState => {
208
+ if (shas.length === 0) return state
209
+ return {
210
+ ...state,
211
+ tickets: mapValues(state.tickets, (progress, number) => {
212
+ const { joined } = progress
213
+ const taken =
214
+ Number(number) !== ticket &&
215
+ joined?.ok === true &&
216
+ joined.shas.some((sha) => shas.includes(sha))
217
+ return taken
218
+ ? { ...progress, joined: null, join_gates: null }
219
+ : progress
220
+ }),
221
+ }
222
+ }
@@ -0,0 +1,86 @@
1
+ import { z } from 'zod'
2
+
3
+ import { ENTRY_FIELDS } from './entry-fields'
4
+
5
+ import {
6
+ DecisionModelOffReasonSchema,
7
+ JevAnswerSchema,
8
+ JevFailureReasonSchema,
9
+ JevFixedSchema,
10
+ JevJobSchema,
11
+ JevRequestSchema,
12
+ } from '../jev/jev-schemas'
13
+
14
+ /**
15
+ * The journal entries of the decision model in shadow mode: each ask, its
16
+ * answers or failure, and the model turned off for the run.
17
+ */
18
+
19
+ /**
20
+ * The engine asked the decision model (Clef; Jev in older journals), in
21
+ * shadow mode, with its own fixed choice beside. `model` is the model asked;
22
+ * journals from before #534 have none. `acted` is true when the answer can
23
+ * change what the engine does, false for a shadow ask; older journals read
24
+ * it as false.
25
+ */
26
+ export const JevAskedEntrySchema = z.object({
27
+ ...ENTRY_FIELDS,
28
+ kind: z.literal('jev_asked'),
29
+ content: z.object({
30
+ job: JevJobSchema,
31
+ request: JevRequestSchema,
32
+ fixed: JevFixedSchema,
33
+ model: z.string().optional(),
34
+ acted: z.boolean().default(false),
35
+ }),
36
+ })
37
+
38
+ /**
39
+ * The decision model's answers to the `jev_asked` record at `asked_seq`.
40
+ * The engine ignores them. `model` is the model that answered, as it named
41
+ * itself (such as `clef`); journals from before #534 have none.
42
+ */
43
+ export const JevAnsweredEntrySchema = z.object({
44
+ ...ENTRY_FIELDS,
45
+ kind: z.literal('jev_answered'),
46
+ content: z.object({
47
+ job: JevJobSchema,
48
+ asked_seq: z.number().int().min(1),
49
+ answers: z.record(z.string(), JevAnswerSchema),
50
+ /** How long the call took. */
51
+ ms: z.number().min(0),
52
+ model: z.string().optional(),
53
+ }),
54
+ })
55
+
56
+ /** The decision model call at `asked_seq` gave no answers. The run goes on regardless. */
57
+ export const JevFailedEntrySchema = z.object({
58
+ ...ENTRY_FIELDS,
59
+ kind: z.literal('jev_failed'),
60
+ content: z.object({
61
+ job: JevJobSchema,
62
+ asked_seq: z.number().int().min(1),
63
+ reason: JevFailureReasonSchema,
64
+ error: z.string(),
65
+ ms: z.number().min(0),
66
+ }),
67
+ })
68
+
69
+ /**
70
+ * The decision model is off for the rest of the run (#534), so the engine
71
+ * asks it nothing more: `no_credentials` at the start (Luca's env file and
72
+ * the process env have no Cloudflare account id or token), or `rejected`
73
+ * after an ask Cloudflare turned down. `model` is the model it would have
74
+ * asked; `detail` is one plain sentence (the file and keys to add, or
75
+ * Cloudflare's answer), never the token. Shadow mode only: it changes
76
+ * nothing in the run.
77
+ */
78
+ export const DecisionModelOffEntrySchema = z.object({
79
+ ...ENTRY_FIELDS,
80
+ kind: z.literal('decision_model_off'),
81
+ content: z.object({
82
+ model: z.string(),
83
+ reason: DecisionModelOffReasonSchema,
84
+ detail: z.string(),
85
+ }),
86
+ })
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod'
2
+
3
+ /** The fields every journal entry has, whatever its kind. */
4
+
5
+ export const ENTRY_FIELDS = {
6
+ /** The ticket (or spec) a record is about, `null` for the whole run. */
7
+ ticket: z.number().int().positive().nullable(),
8
+ /** The role that produced the record, `null` for the engine itself. */
9
+ role: z.string().nullable(),
10
+ }