@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
@@ -1,755 +1,90 @@
1
- import mapValues from 'lodash/mapValues'
2
- import omit from 'lodash/omit'
3
- import omitBy from 'lodash/omitBy'
4
1
  import uniq from 'lodash/uniq'
5
2
 
6
- import type {
7
- AgentFailure,
8
- CommitStage,
9
- JournalRecord,
10
- RunStuckReason,
11
- StuckReason,
12
- UsageLineWindow,
13
- } from './journal-record'
14
- import { crashesAfter, type CrashCounts } from './step-records'
15
-
16
- import type { RejoinContext } from '../agents/role-prompts'
17
3
  import {
18
- AgentRoleSchema,
19
- isBlocking,
20
- lensOf,
21
- type AgentRole,
22
- type BadTest,
23
- type Finding,
24
- type FindingResponse,
25
- type ImplementerResult,
26
- type LearnerResult,
27
- type LensName,
28
- type LensReviewResult,
29
- type TestWriterResult,
30
- type TicketReviewResult,
31
- } from '../agents/role-results'
32
- import type { EngineConfig } from '../config/engine-config'
33
- import type {
34
- GateCheck,
35
- LeftoverHit,
36
- RedCheckResult,
37
- TestRun,
38
- } from '../gates/gate-schemas'
39
- import type {
40
- IntakeProblem,
41
- IntakeRead,
42
- SpecSnapshot,
43
- TicketSnapshot,
44
- } from '../intake/intake-schemas'
45
- import { sessionSignal } from '../limits/plan-signals'
46
- import type {
47
- MemoryFeedback,
48
- MemorySave,
49
- RecallPoint,
50
- RecalledMemory,
51
- } from '../memory/memory-schemas'
52
-
53
- /** Where a run stands, as far as intake goes. */
54
- export type RunPhase =
55
- | 'new'
56
- | 'started'
57
- | 'intake_read'
58
- | 'refused'
59
- | 'nothing_to_do'
60
- /** The spec snapshot is written but some ticket snapshots are not. */
61
- | 'snapshotting'
62
- | 'intake_passed'
63
-
64
- /** The snapshot as replayed from `spec_snapshot` and `ticket_snapshot`. */
65
- export type ReplayedSnapshot = {
66
- spec: SpecSnapshot
67
- ticket_order: number[]
68
- closed_tickets: number[]
69
- /** The latest snapshot of each ticket, keyed by ticket number. */
70
- tickets: Record<number, TicketSnapshot>
71
- }
72
-
73
- /** A worktree the engine made, as journaled. */
74
- export type ReplayedWorktree = {
75
- branch: string
76
- path: string
77
- base_sha: string
78
- }
79
-
80
- /**
81
- * A ticket sent back to be fixed on top of the run branch (`ticket_rebased`):
82
- * why, where it starts now, the files that clashed, and the findings known
83
- * from its reviews. `tests_pending` and `code_pending` stay true until a
84
- * fresh test-writer (the clashed tests) or the implementer (the clashed
85
- * code) has answered.
86
- */
87
- export type ReplayedRejoin = RejoinContext & {
88
- tests_pending: boolean
89
- code_pending: boolean
90
- }
91
-
92
- /** One gate run, as journaled. */
93
- export type ReplayedGates = { ok: boolean; checks: GateCheck[] }
94
-
95
- /** The red check's verdict, plus the test run's (clipped) output. */
96
- export type ReplayedRedCheck = RedCheckResult & { output: string }
97
-
98
- /**
99
- * The open review fix round: the blockers and should-fixes the latest ticket
100
- * review sent back, and which fixers have answered. Test findings go to a
101
- * fresh test-writer first, then code findings to the implementer.
102
- */
103
- export type ReviewFix = {
104
- /** 1 after the first review, 2 after the first re-review, ... */
105
- round: number
106
- findings: Finding[]
107
- /** True once the test-writer answered, or when no finding is a test finding. */
108
- tests_answered: boolean
109
- /** True once the implementer answered, or when no finding is a code finding. */
110
- code_answered: boolean
111
- /** The fixers' answers, oldest first. */
112
- responses: FindingResponse[]
113
- /** A test the implementer sent back as bad while fixing, if any. */
114
- bad_test: BadTest | null
115
- }
116
-
117
- /**
118
- * A finding a fixer answered "won't fix" and the next reviewer let go: its
119
- * ruling was `accepted`, or it gave none and did not list the finding again.
120
- */
121
- export type DeclinedFinding = {
122
- finding: Finding
123
- /** The fixer's reason. */
124
- reason: string
125
- /** The reviewer's reason for accepting it, if it gave one. */
126
- ruling: string
127
- }
128
-
129
- /**
130
- * How far one ticket got, from its journal records. Each field holds the
131
- * latest record of its kind; `null` means the step has not finished.
132
- *
133
- * The fix-loop counts are derived from record order, never written by hand,
134
- * so a crashed run counts them the same way again: a test-writer result that
135
- * comes after a red check answers a fix round (and the red check runs again),
136
- * and an implementer result after the gates does the same for the gates.
137
- */
138
- export type TicketProgress = {
139
- worktree: ReplayedWorktree | null
140
- /** The install in the new worktree; its `check` is `null` with no manifest. */
141
- install: ReplayedInstall | null
142
- /** The prepare command run before its baseline tests (`baseline_prepared`). */
143
- prepare: GateCheck | null
144
- /** Its own baseline test run, or the one it reused (`baseline_reused`). */
145
- baseline: TestRun | null
146
- /**
147
- * The run-branch commit the baseline was taken at. It stays when a rebase
148
- * moves the worktree, so the baseline is only lent to tickets from there.
149
- */
150
- baseline_sha: string | null
151
- test_writer: TestWriterResult | null
152
- red_check: ReplayedRedCheck | null
153
- /** Follow-ups the test-writer answered after a failed red check. */
154
- red_fix_rounds: number
155
- implementer: ImplementerResult | null
156
- /** Follow-ups the implementer answered after failed gates. */
157
- gate_fix_rounds: number
158
- /** How many times an implementer sent a test back as bad. */
159
- bad_test_bounces: number
160
- /** The latest test sent back as bad, for the fresh test-writer's prompt. */
161
- bad_test: BadTest | null
162
- /** The session each role's latest turn ran in, for follow-ups. */
163
- sessions: Partial<Record<AgentRole, string>>
164
- /** Every assumption any agent on the ticket made, oldest first. */
165
- assumptions: string[]
166
- /** The latest ticket review's result. */
167
- review: TicketReviewResult | null
168
- /** Ticket reviews finished on this ticket. */
169
- review_rounds: number
170
- /** The commit the latest review looked at. */
171
- reviewed_sha: string | null
172
- /** The open review fix round; `null` before a review or once approved. */
173
- review_fix: ReviewFix | null
174
- /** Every nit reviewers reported, oldest first, one per id. */
175
- nits: Finding[]
176
- /** Findings declined through "won't fix", oldest first. */
177
- declined: DeclinedFinding[]
178
- /**
179
- * The seq of the review that finally approved the ticket (no blocker or
180
- * should-fix left), `null` until one does. Approved tickets join the run
181
- * branch in this order, one at a time.
182
- */
183
- approved_seq: number | null
184
- /** How many times the ticket was sent back onto the run branch. */
185
- rejoins: number
186
- /** The latest time it was sent back, `null` if it never was. */
187
- rejoin: ReplayedRejoin | null
188
- /**
189
- * The latest failed agent turn, if no agent finished after it: its role,
190
- * error, how it failed, and the session it ran in (for a follow-up).
191
- */
192
- agent_failure: {
193
- role: AgentRole
194
- error: string
195
- failure: AgentFailure
196
- session_id: string | null
197
- } | null
198
- /**
199
- * Failed tries (agent, result, and guard failures) each role has used on
200
- * this ticket, in all. Engine failures never count here.
201
- */
202
- failed_tries: Partial<Record<AgentRole, number>>
203
- /** Engine failures in a row; any other turn's end resets it. */
204
- engine_failures: number
205
- leftovers: Record<CommitStage, LeftoverHit[] | null>
206
- commits: Record<CommitStage, string | null>
207
- /** The files each stage's latest commit changed. */
208
- commit_files: Record<CommitStage, string[]>
209
- gates: ReplayedGates | null
210
- joined: { ok: true; shas: string[] } | { ok: false; error: string } | null
211
- join_gates: ReplayedGates | null
212
- pushed: string | null
213
- stuck: ReplayedStuck | null
214
- /** The spec issue's comment that told the owner, once posted. */
215
- stuck_report: { comment_id: number } | null
216
- /** The owner's reply to this stuck ticket, until the engine acts on it. */
217
- reply: { word: 'retry' | 'skip'; comment_id: number } | null
218
- /** Left out of the run: skipped by reply, or waits on a skipped ticket. */
219
- skipped: { because: number | null } | null
220
- /**
221
- * Its work was already on the base branch, done by these commits (#484):
222
- * the ticket is done without a join, like a pushed one.
223
- */
224
- already_done: { shas: string[] } | null
225
- /** The engine closed it (an already-done ticket in a run with no PR). */
226
- closed: boolean
227
- /**
228
- * Why the ticket got stuck before its latest `retry` resumed it; the next
229
- * fresh agent is told. Cleared once an agent starts.
230
- */
231
- retried: ReplayedStuck | null
232
- /** A test setup file change an agent asked for; the ticket is stuck on it. */
233
- setup_change: { role: AgentRole; file: string; reason: string } | null
234
- /**
235
- * The role whose turn a crash cut off (`run_resumed`), so the fresh
236
- * agent that takes it again is told. Cleared once an agent starts.
237
- */
238
- crashed_turn: AgentRole | null
239
- }
240
-
241
- /** Why a ticket is stuck, as journaled. */
242
- export type ReplayedStuck = { reason: StuckReason; detail: string }
243
-
244
- /** A comment the engine read on the spec issue. */
245
- export type ReplayedComment = {
246
- comment_id: number
247
- author: string
248
- body: string
249
- }
250
-
251
- /** A final review finding: its id namespaced as `<lens>-<id>`, and its lens. */
252
- export type FinalFinding = Finding & { lens: LensName }
253
-
254
- /** A finding declined in the final review, with the lens that let it go. */
255
- export type FinalDeclined = DeclinedFinding & { lens: LensName }
256
-
257
- /** A rule file for the rules lens, as the engine read it (`null`: unreadable). */
258
- export type RuleFile = { path: string; text: string | null }
259
-
260
- /** A failed agent turn no agent of that role finished after. */
261
- export type ReplayedAgentFailure = {
262
- role: AgentRole
263
- error: string
264
- failure: AgentFailure
265
- session_id: string | null
266
- }
267
-
268
- /**
269
- * The open final review fix round: the blocking findings of every lens due
270
- * in the round, and which fixers answered. Like a ticket's `ReviewFix`.
271
- */
272
- export type FinalReviewFix = Omit<ReviewFix, 'findings'> & {
273
- findings: FinalFinding[]
274
- }
275
-
276
- /**
277
- * Where the final review stands, from its records: its round, the lenses
278
- * due, each lens's result this round, the open fix round and its fixers,
279
- * gates, commit, and push, and what goes in the PR. Ticket-less agent,
280
- * gate, scan, commit, and push records belong here once
281
- * `final_review_started` is journaled.
282
- */
283
- export type FinalReviewState = {
284
- /** 0 before the final review starts, then 1, 2, ... */
285
- round: number
286
- from_sha: string | null
287
- head_sha: string | null
288
- /** The files `git diff <from_sha>..<head_sha>` changes. */
289
- files: string[]
290
- /** The lenses due this round. */
291
- lenses_due: LensName[]
292
- rules: RuleFile[]
293
- /** Each due lens's result this round, finding ids namespaced. */
294
- results: Partial<Record<LensName, LensReviewResult>>
295
- /** The open fix round; `null` before the first review settles or once clean. */
296
- fix: FinalReviewFix | null
297
- /** `final_review_fixing` is journaled for the open fix round. */
298
- fixing_started: boolean
299
- /** The session of each role's latest turn, for follow-ups. */
300
- sessions: Partial<Record<AgentRole, string>>
301
- /** Each role's failed turn, if no agent of that role finished after it. */
302
- agent_failures: Partial<Record<AgentRole, ReplayedAgentFailure>>
303
- /** Failed tries (agent, result, guard) per role, over the whole final review. */
304
- failed_tries: Partial<Record<AgentRole, number>>
305
- /** Engine failures in a row, per role. */
306
- engine_failures: Partial<Record<AgentRole, number>>
307
- /** The open fix round's gates, and its gate fix follow-ups. */
308
- gates: ReplayedGates | null
309
- gate_fix_rounds: number
310
- /** The open fix round's leftover scan and commit. */
311
- leftovers: LeftoverHit[] | null
312
- commit: { sha: string; files: string[] } | null
313
- /** The open fix round's push. */
314
- pushed: string | null
315
- /** Every nit the lenses reported, one per id. */
316
- nits: FinalFinding[]
317
- declined: FinalDeclined[]
318
- /** Every assumption a final review agent made, oldest first. */
319
- assumptions: string[]
320
- passed: boolean
321
- stuck: { reason: StuckReason; detail: string } | null
322
- shipped: boolean
323
- /** The fixer whose turn a crash cut off; cleared once a fixer starts. */
324
- crashed_turn: AgentRole | null
325
- }
326
-
327
- /** The final review before it starts. */
328
- export const EMPTY_FINAL_REVIEW: FinalReviewState = {
329
- round: 0,
330
- from_sha: null,
331
- head_sha: null,
332
- files: [],
333
- lenses_due: [],
334
- rules: [],
335
- results: {},
336
- fix: null,
337
- fixing_started: false,
338
- sessions: {},
339
- agent_failures: {},
340
- failed_tries: {},
341
- engine_failures: {},
342
- gates: null,
343
- gate_fix_rounds: 0,
344
- leftovers: null,
345
- commit: null,
346
- pushed: null,
347
- nits: [],
348
- declined: [],
349
- assumptions: [],
350
- passed: false,
351
- stuck: null,
352
- shipped: false,
353
- crashed_turn: null,
354
- }
355
-
356
- /** The engine's install in a new worktree: `null` check means nothing to install. */
357
- export type ReplayedInstall = { check: GateCheck | null }
358
-
359
- /** The run's pull request, once opened. */
360
- export type ReplayedPullRequest = { number: number; url: string }
361
-
362
- /**
363
- * Where the run stands with the plan, from agents' rate-limit readings and
364
- * the limit-wait and stop records.
365
- */
366
- export type PlanState = {
367
- /**
368
- * The latest rejected rate limit no limit wait has started for yet: the
369
- * window, its reset (seconds since the epoch), the agent that hit it, and
370
- * when the session was journaled.
371
- */
372
- hit: {
373
- rate_limit_type: string | null
374
- resets_at: number | null
375
- ticket: number | null
376
- role: AgentRole | null
377
- time: string
378
- } | null
379
- /** The limit wait under way: when it ends, and what it is for. */
380
- wait: {
381
- until: string
382
- resets_at: string | null
383
- rate_limit_type: string | null
384
- } | null
385
- /** Every limit wait's reset so far, oldest first; the spec hears of each new one once. */
386
- resets_announced: (string | null)[]
387
- /** The first sign of per-token billing in any session, if any. */
388
- billing: { reason: string } | null
389
- /** Set once a billing stop is journaled. It sticks. */
390
- billing_stopped: { reason: string } | null
391
- /** The usage-line wait under way: its window, line, reading, and end. */
392
- usage_wait: {
393
- window: UsageLineWindow
394
- line: number
395
- percent: number
396
- until: string
397
- } | null
398
- /**
399
- * The latest time the run is known to have waited until: the `until` of
400
- * a limit wait that ended, or of a usage-line wait that ended at its
401
- * reset. A reading whose window resets by then is out of date.
402
- */
403
- waited_until: string | null
404
- }
405
- /**
406
- * The learner at the end of a run (#370): its answer, or its failed tries,
407
- * or that the engine gave up on it. Learner records have `ticket: null` and
408
- * role `learner`; they never count as the final review's.
409
- */
410
- export type LearnerState = {
411
- /** Its result, once it finished. */
412
- result: LearnerResult | null
413
- /** The seq of its `agent_finished`, for the saves' op ids. */
414
- result_seq: number | null
415
- /** Its latest failed turn, if none finished after it. */
416
- agent_failure: ReplayedAgentFailure | null
417
- /** Failed tries (agent, result, guard), in all. */
418
- failed_tries: number
419
- /** Engine failures in a row. */
420
- engine_failures: number
421
- /** Why the engine gave up on it (`learning_skipped`), if it did. */
422
- skipped: string | null
423
- }
424
-
425
- /** Where memory stands in a run (#370), from its records. */
426
- export type MemoryState = {
427
- /** Memory is on for this run (`run_started.memory`). */
428
- on: boolean
429
- /** The project's vault; `null` searches only `default`. */
430
- project_vault: string | null
431
- /** Each recall point's memories, by its key, once searched. */
432
- recalls: Record<string, { point: RecallPoint; memories: RecalledMemory[] }>
433
- /** Every memory shown in the run, oldest first; may repeat. */
434
- shown: RecalledMemory[]
435
- learner: LearnerState
436
- /** The learner's memories as saved, and the feedback sent. */
437
- saved: { saves: MemorySave[]; feedback: MemoryFeedback[] } | null
438
- /** The new memories went on the spec issue (a run with no PR). */
439
- reported: boolean
440
- }
441
-
442
- /** A run with memory off, before any memory record. */
443
- export const EMPTY_MEMORY: MemoryState = {
444
- on: false,
445
- project_vault: null,
446
- recalls: {},
447
- shown: [],
448
- learner: {
449
- result: null,
450
- result_seq: null,
451
- agent_failure: null,
452
- failed_tries: 0,
453
- engine_failures: 0,
454
- skipped: null,
455
- },
456
- saved: null,
457
- reported: false,
458
- }
459
-
460
- /** One run note, with the agent that wrote it. */
461
- export type ReplayedRunNote = { ticket: number; role: AgentRole; note: string }
462
-
463
- /**
464
- * The run budget (#435): whether the run is stuck on it and told, and how
465
- * many more full budgets the owner's `retry` replies added.
466
- */
467
- export type RunBudgetState = {
468
- /** The run is stuck, until the owner's `retry`. */
469
- stuck: { reason: RunStuckReason; detail: string } | null
470
- /** The spec issue's comment that told the owner the run is stuck. */
471
- report: { comment_id: number } | null
472
- /** Full budgets added by `retry`: the run may use `1 + retries` budgets. */
473
- retries: number
474
- }
475
-
476
- /** A run's state, rebuilt only from its journal. There is no status file. */
477
- export type RunState = {
478
- phase: RunPhase
479
- spec_number: number | null
480
- config: EngineConfig | null
481
- base_branch: string | null
482
- intake: IntakeRead | null
483
- problems: IntakeProblem[] | null
484
- snapshot: ReplayedSnapshot | null
485
- run_branch: ReplayedWorktree | null
486
- /** The install in the run branch's checkout, once it ran. */
487
- run_branch_install: ReplayedInstall | null
488
- tickets: Record<number, TicketProgress>
489
- /**
490
- * The repo had a changesets config when the run branch was made, so a
491
- * changeset is written before the PR (#461).
492
- */
493
- changesets: boolean
494
- /** The run's changeset, once written and pushed. */
495
- changeset: { path: string | null; sha: string } | null
496
- pull_request: ReplayedPullRequest | null
497
- plan: PlanState
498
- /**
499
- * The seq of each ticket's latest usage record (a retried ticket that
500
- * finishes again gets another), and whether the run's is recorded.
501
- */
502
- usage_recorded: { tickets: Record<number, number>; run: boolean }
503
- /** The worktrees the engine removed at the end of the run. */
504
- removed_worktrees: string[]
505
- /**
506
- * Every run note from every finished agent in the run, oldest first
507
- * (journal order), word for word.
508
- */
509
- run_notes: ReplayedRunNote[]
510
- /** The latest gates on the run branch: after a join, or a final review fix. */
511
- run_branch_gates: ReplayedGates | null
512
- /**
513
- * Every untracked path a prepare run made in any of the run's checkouts
514
- * (`prepare_made`), in journal order. Never an agent's work: the engine
515
- * leaves them out of commits, the leftover scan, and role rules.
516
- */
517
- prepare_made: string[]
518
- final_review: FinalReviewState
519
- /** Comments read on the spec issue while waiting for replies, oldest first. */
520
- comments: ReplayedComment[]
521
- /** Comments already taken as a reply, or sent back. */
522
- handled_comments: number[]
523
- /** Comments the engine itself posted on the spec issue. */
524
- engine_comments: number[]
525
- /** The owner replied `stop`: nothing new starts, and the run ends without a PR. */
526
- stop: { comment_id: number } | null
527
- /** The spec issue's comment that told the owner the final review is stuck. */
528
- final_stuck_report: { comment_id: number } | null
529
- /** The owner's reply to the stuck final review, until the engine acts on it. */
530
- final_reply: { word: 'retry' | 'ship'; comment_id: number } | null
531
- /** The run's budget of tokens: stuck on it, and the budgets added (#435). */
532
- run_budget: RunBudgetState
533
- /** Memory's recalls, the learner, and the saves (#370). */
534
- memory: MemoryState
535
- /**
536
- * Crashes in a row per scheduler key (`run_resumed`), until a step under
537
- * the key ends. A ticket's `retry`, or the final review's, clears its own.
538
- */
539
- crashes: CrashCounts
540
- /** Set once a stop for crashes is journaled. It sticks. */
541
- crash_stopped: { reason: string } | null
542
- last_seq: number
543
- }
544
-
545
- /** A ticket nothing has happened to yet. */
546
- export const EMPTY_TICKET_PROGRESS: TicketProgress = {
547
- worktree: null,
548
- install: null,
549
- prepare: null,
550
- baseline: null,
551
- baseline_sha: null,
552
- test_writer: null,
553
- red_check: null,
554
- red_fix_rounds: 0,
555
- implementer: null,
556
- gate_fix_rounds: 0,
557
- bad_test_bounces: 0,
558
- bad_test: null,
559
- sessions: {},
560
- assumptions: [],
561
- review: null,
562
- review_rounds: 0,
563
- reviewed_sha: null,
564
- review_fix: null,
565
- nits: [],
566
- declined: [],
567
- approved_seq: null,
568
- rejoins: 0,
569
- rejoin: null,
570
- agent_failure: null,
571
- failed_tries: {},
572
- engine_failures: 0,
573
- leftovers: { red: null, green: null, fix: null },
574
- commits: { red: null, green: null, fix: null },
575
- commit_files: { red: [], green: [], fix: [] },
576
- gates: null,
577
- joined: null,
578
- join_gates: null,
579
- pushed: null,
580
- stuck: null,
581
- stuck_report: null,
582
- reply: null,
583
- skipped: null,
584
- already_done: null,
585
- closed: false,
586
- retried: null,
587
- setup_change: null,
588
- crashed_turn: null,
589
- }
590
-
591
- const EMPTY_STATE: RunState = {
592
- phase: 'new',
593
- spec_number: null,
594
- config: null,
595
- base_branch: null,
596
- intake: null,
597
- problems: null,
598
- snapshot: null,
599
- run_branch: null,
600
- run_branch_install: null,
601
- tickets: {},
602
- changesets: false,
603
- changeset: null,
604
- pull_request: null,
605
- plan: {
606
- hit: null,
607
- wait: null,
608
- resets_announced: [],
609
- billing: null,
610
- billing_stopped: null,
611
- usage_wait: null,
612
- waited_until: null,
613
- },
614
- usage_recorded: { tickets: {}, run: false },
615
- removed_worktrees: [],
616
- run_notes: [],
617
- run_branch_gates: null,
618
- prepare_made: [],
619
- final_review: EMPTY_FINAL_REVIEW,
620
- comments: [],
621
- handled_comments: [],
622
- engine_comments: [],
623
- stop: null,
624
- final_stuck_report: null,
625
- final_reply: null,
626
- run_budget: { stuck: null, report: null, retries: 0 },
627
- memory: EMPTY_MEMORY,
628
- crashes: {},
629
- crash_stopped: null,
630
- last_seq: 0,
631
- }
632
-
633
- const snapshotPhase = ({
634
- snapshot,
635
- }: {
636
- snapshot: ReplayedSnapshot
637
- }): RunPhase =>
638
- snapshot.ticket_order.every((number) => number in snapshot.tickets)
639
- ? 'intake_passed'
640
- : 'snapshotting'
641
-
642
- type PlanRecord = Extract<
643
- JournalRecord,
644
- {
645
- kind:
646
- | 'agent_session'
647
- | 'run_stopped'
648
- | 'limit_wait_started'
649
- | 'limit_wait_ended'
650
- | 'usage_line_wait_started'
651
- | 'usage_line_wait_ended'
652
- }
653
- >
654
-
655
- /** The later of two ISO times; the first may be missing. */
656
- const laterOf = (a: string | null, b: string): string =>
657
- a !== null && Date.parse(a) >= Date.parse(b) ? a : b
658
-
659
- /** The plan state after one record. */
660
- const planAfter = ({
661
- plan,
662
- record,
663
- }: {
664
- plan: PlanState
665
- record: PlanRecord
666
- }): PlanState => {
667
- switch (record.kind) {
668
- case 'agent_session': {
669
- const signal = sessionSignal({ session: record.content.session })
670
- if (signal.kind === 'billing') {
671
- return {
672
- ...plan,
673
- billing: plan.billing ?? { reason: signal.reason },
674
- }
675
- }
676
- if (signal.kind === 'ok') return plan
677
- return {
678
- ...plan,
679
- hit: {
680
- rate_limit_type: signal.rate_limit_type,
681
- resets_at: signal.resets_at,
682
- ticket: record.ticket,
683
- role: record.content.role,
684
- time: record.time,
685
- },
686
- }
687
- }
688
- case 'run_stopped':
689
- return record.content.billing
690
- ? {
691
- ...plan,
692
- billing: plan.billing ?? {
693
- reason: record.content.reason,
694
- },
695
- billing_stopped: { reason: record.content.reason },
696
- }
697
- : plan
698
- case 'limit_wait_started': {
699
- const { until, resets_at, rate_limit_type } = record.content
700
- return {
701
- ...plan,
702
- hit: null,
703
- wait: { until, resets_at, rate_limit_type },
704
- resets_announced: [...plan.resets_announced, resets_at],
705
- }
706
- }
707
- case 'limit_wait_ended':
708
- return {
709
- ...plan,
710
- wait: null,
711
- waited_until: laterOf(plan.waited_until, record.content.until),
712
- }
713
- case 'usage_line_wait_started': {
714
- const { window, line, percent, until } = record.content
715
- return { ...plan, usage_wait: { window, line, percent, until } }
716
- }
717
- case 'usage_line_wait_ended':
718
- return {
719
- ...plan,
720
- usage_wait: null,
721
- waited_until:
722
- record.content.reason === 'reset'
723
- ? laterOf(plan.waited_until, record.content.until)
724
- : plan.waited_until,
725
- }
726
- }
727
- }
4
+ closedSessionsAfter,
5
+ goneSessionOf,
6
+ resumedAfter,
7
+ undoneJoinsAfter,
8
+ } from './cross-ticket-replay'
9
+ import {
10
+ finalRecordOf,
11
+ finalRepliesAfter,
12
+ finalReviewAfter,
13
+ omitFinalKeys,
14
+ resumedFinalReview,
15
+ } from './final-review-replay'
16
+ import type { JournalRecord } from './journal-record'
17
+ import { EMPTY_MEMORY, learnerAfter, learnerRecordOf } from './memory-replay'
18
+ import { planAfter } from './plan-replay'
19
+ import {
20
+ replyReceivedAfter,
21
+ stuckReportedAfter,
22
+ ticketRetriedAfter,
23
+ } from './reply-replay'
24
+ import {
25
+ EMPTY_STATE,
26
+ engineCommentsAfter,
27
+ notesAfter,
28
+ snapshotPhase,
29
+ type RunState,
30
+ } from './run-state'
31
+ import { sharedFailuresAfter } from './shared-failure-replay'
32
+ import { crashesAfter } from './step-records'
33
+ import { applyTicketRecord, reusedBaselineAfter } from './ticket-replay'
34
+
35
+ import { withConfigChanges } from '../config/config-reload'
36
+
37
+ export type {
38
+ OldTestFailure,
39
+ RunBudgetState,
40
+ ReplayedComment,
41
+ ReplayedPullRequest,
42
+ ReplayedRunNote,
43
+ ReplayedSnapshot,
44
+ RunPhase,
45
+ RunState,
46
+ } from './run-state'
47
+ export type {
48
+ DeclinedFinding,
49
+ EnvironmentRerun,
50
+ ReplayedGates,
51
+ ReplayedInstall,
52
+ ReplayedRedCheck,
53
+ ReplayedRejoin,
54
+ ReplayedStuck,
55
+ ReplayedWorktree,
56
+ ReviewFix,
57
+ TestUpdate,
58
+ TicketProgress,
59
+ WriterRole,
60
+ } from './ticket-progress'
61
+ export { EMPTY_TICKET_PROGRESS } from './ticket-progress'
62
+ export type {
63
+ FinalDeclined,
64
+ FinalFinding,
65
+ FinalReviewFix,
66
+ FinalReviewState,
67
+ ReplayedAgentFailure,
68
+ RuleFile,
69
+ } from './final-review-state'
70
+ export { EMPTY_FINAL_REVIEW } from './final-review-state'
71
+ export type { PlanState } from './plan-replay'
72
+ export type { LearnerState, MemoryState } from './memory-replay'
73
+ export { EMPTY_MEMORY } from './memory-replay'
74
+ export { namespacedId } from './final-review-round'
75
+ export { isFinalKey } from './final-review-replay'
76
+ export { handoffRole } from './agent-turn-replay'
728
77
 
729
- /** The run's notes after an agent finished: its own go at the end. */
730
- const notesAfter = ({
78
+ const applyRecord = ({
731
79
  state,
732
80
  record,
733
81
  }: {
734
82
  state: RunState
735
- record: Extract<JournalRecord, { kind: 'agent_finished' }>
736
- }): ReplayedRunNote[] => {
737
- const { ticket, content } = record
738
- // Reviewers (the ticket reviewer and the lenses) leave no run notes.
739
- if (ticket === null || !('run_notes' in content.result)) {
740
- return state.run_notes
741
- }
742
- return [
743
- ...state.run_notes,
744
- ...content.result.run_notes.map((note) => ({
745
- ticket,
746
- role: content.role,
747
- note,
748
- })),
749
- ]
750
- }
83
+ record: JournalRecord
84
+ }): RunState =>
85
+ closedSessionsAfter({ state: applyKind({ state, record }), record })
751
86
 
752
- const applyRecord = ({
87
+ const applyKind = ({
753
88
  state,
754
89
  record,
755
90
  }: {
@@ -761,6 +96,10 @@ const applyRecord = ({
761
96
  last_seq: record.seq,
762
97
  crashes: crashesAfter({ crashes: state.crashes, record }),
763
98
  }
99
+ // A follow-up found its session gone (#521): only the session is
100
+ // forgotten (see `closedSessionsAfter`), so its fix round goes to a
101
+ // fresh agent, and no engine failure is counted.
102
+ if (goneSessionOf(record) !== null) return base
764
103
  const next =
765
104
  record.kind === 'gates_run' && record.content.target === 'run_branch'
766
105
  ? {
@@ -877,6 +216,7 @@ const applyRecord = ({
877
216
  ticket_order: record.content.ticket_order,
878
217
  closed_tickets: record.content.closed_tickets,
879
218
  tickets: {},
219
+ left_out: record.content.left_out,
880
220
  }
881
221
  return { ...next, snapshot, phase: snapshotPhase({ snapshot }) }
882
222
  }
@@ -933,10 +273,12 @@ const applyRecord = ({
933
273
  url: record.content.url,
934
274
  },
935
275
  }
936
- // Jev's shadow-mode records change nothing: the engine ignores them.
276
+ // The decision model's shadow-mode records change nothing: the
277
+ // engine ignores them.
937
278
  case 'jev_asked':
938
279
  case 'jev_answered':
939
280
  case 'jev_failed':
281
+ case 'decision_model_off':
940
282
  return next
941
283
  // A session's readings and the stops and limit waits change no
942
284
  // ticket: the step they cut off is picked up again afterwards.
@@ -946,6 +288,21 @@ const applyRecord = ({
946
288
  case 'usage_line_wait_started':
947
289
  case 'usage_line_wait_ended':
948
290
  return { ...next, plan: planAfter({ plan: state.plan, record }) }
291
+ case 'repo_wait_started':
292
+ return {
293
+ ...next,
294
+ repo_wait: { status: 'waiting', since: record.time },
295
+ }
296
+ case 'repo_wait_ended':
297
+ return {
298
+ ...next,
299
+ repo_wait: { ...state.repo_wait, status: 'ended' },
300
+ }
301
+ case 'repo_wait_skipped':
302
+ return {
303
+ ...next,
304
+ repo_wait: { ...state.repo_wait, status: 'skipped' },
305
+ }
949
306
  case 'run_stopped':
950
307
  return {
951
308
  ...next,
@@ -958,6 +315,9 @@ const applyRecord = ({
958
315
  case 'step_started':
959
316
  case 'step_ended':
960
317
  return next
318
+ // A wait between steps changes no step: only the board reads it.
319
+ case 'ticket_waiting':
320
+ return next
961
321
  // Only a join's redo reads it (see `openJoin`).
962
322
  case 'join_started':
963
323
  return next
@@ -970,6 +330,24 @@ const applyRecord = ({
970
330
  // Luca's version on a resume changes no step: only the board reads it.
971
331
  case 'engine_resumed':
972
332
  return next
333
+ // A resume's new build fields (#516): every later step uses them.
334
+ case 'config_reloaded':
335
+ return {
336
+ ...next,
337
+ config:
338
+ state.config === null
339
+ ? null
340
+ : withConfigChanges({
341
+ config: state.config,
342
+ changes: record.content.changes,
343
+ }),
344
+ }
345
+ // The run kept its config; only the board and the log read why.
346
+ case 'config_reload_refused':
347
+ return next
348
+ // A failing `ps` changes no step: the wait goes on (#624).
349
+ case 'repo_wait_ps_failed':
350
+ return next
973
351
  case 'comment_read':
974
352
  return { ...next, comments: [...state.comments, record.content] }
975
353
  case 'run_stuck':
@@ -1002,70 +380,10 @@ const applyRecord = ({
1002
380
  review: state.final_review,
1003
381
  }),
1004
382
  }
1005
- case 'reply_received': {
1006
- const handled = {
1007
- ...next,
1008
- handled_comments: [
1009
- ...state.handled_comments,
1010
- record.content.comment_id,
1011
- ],
1012
- }
1013
- const { word, comment_id, ticket } = record.content
1014
- if (word === 'stop') return { ...handled, stop: { comment_id } }
1015
- // A bare `retry` while the run is stuck on its budget adds one
1016
- // more full budget, and the run carries on.
1017
- if (
1018
- ticket === null &&
1019
- word === 'retry' &&
1020
- state.run_budget.stuck !== null
1021
- ) {
1022
- return {
1023
- ...handled,
1024
- run_budget: {
1025
- stuck: null,
1026
- report: null,
1027
- retries: state.run_budget.retries + 1,
1028
- },
1029
- }
1030
- }
1031
- if (ticket === null && (word === 'retry' || word === 'ship')) {
1032
- return { ...handled, final_reply: { word, comment_id } }
1033
- }
1034
- return applyTicketRecord({ state: handled, record })
1035
- }
1036
- case 'stuck_reported': {
1037
- const reported = {
1038
- ...next,
1039
- engine_comments: engineCommentsAfter({
1040
- state,
1041
- comment_id: record.content.comment_id,
1042
- }),
1043
- }
1044
- // A report with no ticket is the stuck run's, while it is not
1045
- // told yet, else the final review's.
1046
- const { run_budget } = state
1047
- if (
1048
- record.ticket === null &&
1049
- run_budget.stuck !== null &&
1050
- run_budget.report === null
1051
- ) {
1052
- return {
1053
- ...reported,
1054
- run_budget: {
1055
- ...run_budget,
1056
- report: { comment_id: record.content.comment_id },
1057
- },
1058
- }
1059
- }
1060
- return record.ticket === null
1061
- ? {
1062
- ...reported,
1063
- final_stuck_report: {
1064
- comment_id: record.content.comment_id,
1065
- },
1066
- }
1067
- : applyTicketRecord({ state: reported, record })
1068
- }
383
+ case 'reply_received':
384
+ return replyReceivedAfter({ state, next, record })
385
+ case 'stuck_reported':
386
+ return stuckReportedAfter({ state, next, record })
1069
387
  // Its note on the spec issue is the engine's own, never a reply.
1070
388
  case 'ticket_already_done':
1071
389
  return applyTicketRecord({
@@ -1078,33 +396,8 @@ const applyRecord = ({
1078
396
  },
1079
397
  record,
1080
398
  })
1081
- case 'ticket_retried': {
1082
- const { answer_id, mode } = record.content
1083
- const install = state.run_branch_install?.check
1084
- // A run branch whose install failed is installed again on a retry.
1085
- const run_branch_install =
1086
- mode === 'refused' ||
1087
- install === undefined ||
1088
- install?.ok !== false
1089
- ? state.run_branch_install
1090
- : null
1091
- return applyTicketRecord({
1092
- state: {
1093
- ...next,
1094
- // A retry gives the ticket fresh crash counts too.
1095
- crashes:
1096
- record.ticket === null
1097
- ? next.crashes
1098
- : omit(next.crashes, String(record.ticket)),
1099
- run_branch_install,
1100
- engine_comments: engineCommentsAfter({
1101
- state,
1102
- comment_id: answer_id,
1103
- }),
1104
- },
1105
- record,
1106
- })
1107
- }
399
+ case 'ticket_retried':
400
+ return ticketRetriedAfter({ state, next, record })
1108
401
  case 'usage_recorded': {
1109
402
  const { scope, ticket } = record.content
1110
403
  const { tickets, run } = state.usage_recorded
@@ -1127,6 +420,13 @@ const applyRecord = ({
1127
420
  // Others' changes to the shared .git: noted, never acted on.
1128
421
  case 'shared_git_changed':
1129
422
  return next
423
+ // A writer's chores (#618): noted; one the engine could not do is
424
+ // the `agent_failed` after it.
425
+ case 'chores_done':
426
+ return next
427
+ // An agent's changeset the engine removed: noted, never acted on.
428
+ case 'changeset_dropped':
429
+ return next
1130
430
  case 'prepare_made':
1131
431
  return {
1132
432
  ...next,
@@ -1135,17 +435,34 @@ const applyRecord = ({
1135
435
  ...record.content.paths,
1136
436
  ]),
1137
437
  }
438
+ // Forgotten in `closedSessionsAfter`, after every record.
1138
439
  case 'agent_session_closed':
1139
- return closedSessionAfter({
1140
- state: next,
1141
- session_id: record.content.session_id,
1142
- })
440
+ return next
1143
441
  case 'agent_finished':
1144
442
  return applyTicketRecord({
1145
443
  state: { ...next, run_notes: notesAfter({ state, record }) },
1146
444
  record,
1147
445
  })
1148
- // Always the final review's (see finalRecordOf above).
446
+ // A ticket's checks also keep the run's old-test failures (#544).
447
+ case 'red_check':
448
+ case 'gates_run':
449
+ return sharedFailuresAfter({
450
+ state: applyTicketRecord({ state: next, record }),
451
+ record,
452
+ })
453
+ case 'join_undone':
454
+ return undoneJoinsAfter({
455
+ state: applyTicketRecord({ state: next, record }),
456
+ ticket: record.ticket,
457
+ shas: record.content.shas,
458
+ })
459
+ case 'ticket_rebased':
460
+ return undoneJoinsAfter({
461
+ state: applyTicketRecord({ state: next, record }),
462
+ ticket: record.ticket,
463
+ shas: record.content.undone,
464
+ })
465
+ // Always the final review's (see `finalRecordOf`).
1149
466
  case 'final_review_started':
1150
467
  case 'lens_started':
1151
468
  case 'lens_finished':
@@ -1159,1310 +476,6 @@ const applyRecord = ({
1159
476
  }
1160
477
  }
1161
478
 
1162
- /** The agent kinds a learner's turn journals. */
1163
- type LearnerRecord = Extract<
1164
- JournalRecord,
1165
- {
1166
- kind:
1167
- | 'agent_started'
1168
- | 'agent_finished'
1169
- | 'agent_failed'
1170
- | 'agent_session'
1171
- }
1172
- >
1173
-
1174
- /** The record if it is one of the learner's agent records, else `null`. */
1175
- const learnerRecordOf = ({
1176
- record,
1177
- }: {
1178
- record: JournalRecord
1179
- }): LearnerRecord | null => {
1180
- switch (record.kind) {
1181
- case 'agent_started':
1182
- case 'agent_finished':
1183
- case 'agent_failed':
1184
- case 'agent_session':
1185
- return record.content.role === 'learner' ? record : null
1186
- default:
1187
- return null
1188
- }
1189
- }
1190
-
1191
- /**
1192
- * The learner after one of its records: its answer ends any failure; a
1193
- * failed turn counts a try (or an engine failure in a row), like any
1194
- * agent's.
1195
- */
1196
- const learnerAfter = ({
1197
- learner,
1198
- record,
1199
- }: {
1200
- learner: LearnerState
1201
- record: LearnerRecord
1202
- }): LearnerState => {
1203
- switch (record.kind) {
1204
- case 'agent_started':
1205
- case 'agent_session':
1206
- return learner
1207
- case 'agent_finished':
1208
- return record.content.role === 'learner'
1209
- ? {
1210
- ...learner,
1211
- result: record.content.result,
1212
- result_seq: record.seq,
1213
- agent_failure: null,
1214
- engine_failures: 0,
1215
- }
1216
- : learner
1217
- case 'agent_failed': {
1218
- const { role, error, failure, session_id } = record.content
1219
- const agent_failure = { role, error, failure, session_id }
1220
- return failure === 'engine'
1221
- ? {
1222
- ...learner,
1223
- agent_failure,
1224
- engine_failures: learner.engine_failures + 1,
1225
- }
1226
- : {
1227
- ...learner,
1228
- agent_failure,
1229
- engine_failures: 0,
1230
- failed_tries: learner.failed_tries + 1,
1231
- }
1232
- }
1233
- }
1234
- }
1235
-
1236
- /** The final review's own kinds, and the kinds it shares with tickets. */
1237
- type FinalRecord = Extract<
1238
- JournalRecord,
1239
- {
1240
- kind:
1241
- | 'final_review_started'
1242
- | 'lens_started'
1243
- | 'lens_finished'
1244
- | 'final_review_fixing'
1245
- | 'final_review_stuck'
1246
- | 'final_review_passed'
1247
- | 'final_review_shipped'
1248
- | 'agent_started'
1249
- | 'agent_finished'
1250
- | 'agent_failed'
1251
- | 'agent_session'
1252
- | 'gates_run'
1253
- | 'leftover_scan'
1254
- | 'commit_made'
1255
- | 'run_branch_pushed'
1256
- }
1257
- >
1258
-
1259
- const FINAL_KINDS = new Set<JournalRecord['kind']>([
1260
- 'final_review_started',
1261
- 'lens_started',
1262
- 'lens_finished',
1263
- 'final_review_fixing',
1264
- 'final_review_stuck',
1265
- 'final_review_passed',
1266
- 'final_review_shipped',
1267
- ])
1268
-
1269
- /** Kinds tickets journal too; ticket-less ones are the final review's once it started. */
1270
- const SHARED_KINDS = new Set<JournalRecord['kind']>([
1271
- 'agent_started',
1272
- 'agent_finished',
1273
- 'agent_failed',
1274
- 'agent_session',
1275
- 'gates_run',
1276
- 'leftover_scan',
1277
- 'commit_made',
1278
- 'run_branch_pushed',
1279
- ])
1280
-
1281
- /**
1282
- * The record if it is the final review's, else `null`: one of its own
1283
- * kinds, or a ticket-less agent, gate, scan, commit, or push record once
1284
- * the final review started. Before that, ticket-less records keep their old
1285
- * meaning (such as the run branch's gates after a join).
1286
- */
1287
- const finalRecordOf = ({
1288
- state,
1289
- record,
1290
- }: {
1291
- state: RunState
1292
- record: JournalRecord
1293
- }): FinalRecord | null => {
1294
- const mine =
1295
- FINAL_KINDS.has(record.kind) ||
1296
- (record.ticket === null &&
1297
- state.final_review.round > 0 &&
1298
- SHARED_KINDS.has(record.kind))
1299
- return mine ? (record as FinalRecord) : null
1300
- }
1301
-
1302
- /**
1303
- * A lens's finding id, namespaced by its lens (`<lens>-<id>`), since lenses
1304
- * choose their ids on their own. An id that already starts with the lens's
1305
- * prefix is kept, so a re-review listing a finding again keeps its id.
1306
- */
1307
- export const namespacedId = ({
1308
- lens,
1309
- id,
1310
- }: {
1311
- lens: LensName
1312
- id: string
1313
- }): string => (id.startsWith(`${lens}-`) ? id : `${lens}-${id}`)
1314
-
1315
- /** A lens's result with every finding and ruling id namespaced. */
1316
- const namespaced = ({
1317
- lens,
1318
- result,
1319
- }: {
1320
- lens: LensName
1321
- result: LensReviewResult
1322
- }): LensReviewResult => ({
1323
- ...result,
1324
- findings: result.findings.map((entry) => ({
1325
- ...entry,
1326
- id: namespacedId({ lens, id: entry.id }),
1327
- })),
1328
- rulings: result.rulings.map((entry) => ({
1329
- ...entry,
1330
- finding_id: namespacedId({ lens, id: entry.finding_id }),
1331
- })),
1332
- })
1333
-
1334
- /**
1335
- * The final review once every due lens finished its round: settles the
1336
- * last fix round's "won't fix" answers per lens (see `declinedBy`), keeps
1337
- * the nits, and opens a fix round on the blocking findings of every due
1338
- * lens, with its fixers, gates, commit, and push afresh (and fresh fixer
1339
- * sessions). No blocking finding leaves `fix` null: the review is clean.
1340
- */
1341
- const settleRound = ({
1342
- review,
1343
- }: {
1344
- review: FinalReviewState
1345
- }): FinalReviewState => {
1346
- const due = review.lenses_due.flatMap((lens) => {
1347
- const result = review.results[lens]
1348
- return result === undefined ? [] : [{ lens, result }]
1349
- })
1350
- const tagged = (lens: LensName, findings: Finding[]): FinalFinding[] =>
1351
- findings.map((entry) => ({ ...entry, lens }))
1352
- const declined = due.flatMap(({ lens, result }) =>
1353
- declinedBy({
1354
- fix:
1355
- review.fix === null
1356
- ? null
1357
- : {
1358
- ...review.fix,
1359
- findings: review.fix.findings.filter(
1360
- (entry) => entry.lens === lens
1361
- ),
1362
- },
1363
- review: result,
1364
- }).map((entry) => ({ ...entry, lens }))
1365
- )
1366
- const nits = due.flatMap(({ lens, result }) =>
1367
- tagged(
1368
- lens,
1369
- result.findings.filter((entry) => !isBlocking(entry))
1370
- )
1371
- )
1372
- const blocking = due.flatMap(({ lens, result }) =>
1373
- tagged(lens, result.findings.filter(isBlocking))
1374
- )
1375
- const settled: FinalReviewState = {
1376
- ...review,
1377
- declined: [...review.declined, ...declined],
1378
- nits: [
1379
- ...review.nits,
1380
- ...nits.filter(
1381
- (entry, index) =>
1382
- !review.nits.some(({ id }) => id === entry.id) &&
1383
- nits.findIndex(({ id }) => id === entry.id) === index
1384
- ),
1385
- ],
1386
- }
1387
- if (blocking.length === 0) return { ...settled, fix: null }
1388
- return {
1389
- ...settled,
1390
- fix: {
1391
- round: review.round,
1392
- findings: blocking,
1393
- tests_answered: !blocking.some(({ kind }) => kind === 'test'),
1394
- code_answered: !blocking.some(({ kind }) => kind === 'code'),
1395
- responses: [],
1396
- bad_test: null,
1397
- },
1398
- fixing_started: false,
1399
- sessions: omit(review.sessions, ['test-writer', 'implementer']),
1400
- gates: null,
1401
- gate_fix_rounds: 0,
1402
- leftovers: null,
1403
- commit: null,
1404
- pushed: null,
1405
- }
1406
- }
1407
-
1408
- /** A fixer's bad test, or the implementer's summary as its reason. */
1409
- const badTestOf = (result: ImplementerResult): BadTest | null =>
1410
- result.outcome === 'bad_test'
1411
- ? (result.bad_test ?? { file: '', name: '', reason: result.summary })
1412
- : null
1413
-
1414
- /**
1415
- * Where a finished agent's result goes in the final review: a lens's
1416
- * result this round (settling the round once every due lens finished), or
1417
- * a fixer's answer to the open fix round. An implementer result after
1418
- * failed gates answers a gate fix round, like a ticket's.
1419
- */
1420
- const finalResultChange = ({
1421
- review,
1422
- finished,
1423
- }: {
1424
- review: FinalReviewState
1425
- finished: FinishedContent
1426
- }): FinalReviewState => {
1427
- const lens = lensOf({ role: finished.role })
1428
- const { fix } = review
1429
- if (lens !== null && 'verdict' in finished.result) {
1430
- if (!review.lenses_due.includes(lens)) return review
1431
- const next = {
1432
- ...review,
1433
- results: {
1434
- ...review.results,
1435
- [lens]: namespaced({ lens, result: finished.result }),
1436
- },
1437
- }
1438
- const pending = next.lenses_due.some(
1439
- (due) => next.results[due] === undefined
1440
- )
1441
- return pending ? next : settleRound({ review: next })
1442
- }
1443
- if (finished.role === 'test-writer') {
1444
- if (fix === null || fix.tests_answered) return review
1445
- return {
1446
- ...review,
1447
- fix: {
1448
- ...fix,
1449
- tests_answered: true,
1450
- responses: [
1451
- ...fix.responses,
1452
- ...finished.result.finding_responses,
1453
- ],
1454
- },
1455
- }
1456
- }
1457
- if (finished.role === 'implementer') {
1458
- const { result } = finished
1459
- if (fix !== null && fix.tests_answered && !fix.code_answered) {
1460
- return {
1461
- ...review,
1462
- fix: {
1463
- ...fix,
1464
- code_answered: true,
1465
- responses: [...fix.responses, ...result.finding_responses],
1466
- bad_test: badTestOf(result),
1467
- },
1468
- }
1469
- }
1470
- if (review.gates === null) return review
1471
- return {
1472
- ...review,
1473
- gates: null,
1474
- gate_fix_rounds: review.gate_fix_rounds + 1,
1475
- }
1476
- }
1477
- return review
1478
- }
1479
-
1480
- /** The final review after one of its records. */
1481
- const finalReviewAfter = ({
1482
- review,
1483
- record,
1484
- }: {
1485
- review: FinalReviewState
1486
- record: FinalRecord
1487
- }): FinalReviewState => {
1488
- switch (record.kind) {
1489
- case 'final_review_started': {
1490
- const { round, from_sha, head_sha, lenses, files, rules } =
1491
- record.content
1492
- return {
1493
- ...review,
1494
- round,
1495
- from_sha,
1496
- head_sha,
1497
- files,
1498
- lenses_due: lenses,
1499
- rules,
1500
- results: {},
1501
- }
1502
- }
1503
- case 'agent_started':
1504
- return isFixerRole(record.content.role)
1505
- ? { ...review, crashed_turn: null }
1506
- : review
1507
- case 'lens_started':
1508
- case 'lens_finished':
1509
- case 'agent_session':
1510
- return review
1511
- case 'final_review_fixing':
1512
- return review.fix !== null &&
1513
- record.content.round === review.fix.round
1514
- ? { ...review, fixing_started: true }
1515
- : review
1516
- case 'final_review_stuck':
1517
- return { ...review, stuck: record.content }
1518
- case 'final_review_passed':
1519
- return { ...review, passed: true }
1520
- case 'final_review_shipped':
1521
- return review.stuck === null ? review : { ...review, shipped: true }
1522
- case 'agent_finished': {
1523
- const finished = record.content
1524
- const { role } = finished
1525
- return finalResultChange({
1526
- review: {
1527
- ...review,
1528
- agent_failures: omit(review.agent_failures, role),
1529
- engine_failures: omit(review.engine_failures, role),
1530
- sessions: sessionsAfter({
1531
- sessions: review.sessions,
1532
- role,
1533
- session_id: finished.session_id,
1534
- }),
1535
- assumptions: [
1536
- ...review.assumptions,
1537
- ...assumptionsOf(finished),
1538
- ],
1539
- },
1540
- finished,
1541
- })
1542
- }
1543
- case 'agent_failed': {
1544
- const { role, error, failure, session_id } = record.content
1545
- const agent_failures = {
1546
- ...review.agent_failures,
1547
- [role]: { role, error, failure, session_id },
1548
- }
1549
- if (failure === 'engine') {
1550
- return {
1551
- ...review,
1552
- agent_failures,
1553
- engine_failures: {
1554
- ...review.engine_failures,
1555
- [role]: (review.engine_failures[role] ?? 0) + 1,
1556
- },
1557
- }
1558
- }
1559
- return {
1560
- ...review,
1561
- agent_failures,
1562
- engine_failures: omit(review.engine_failures, role),
1563
- failed_tries: {
1564
- ...review.failed_tries,
1565
- [role]: (review.failed_tries[role] ?? 0) + 1,
1566
- },
1567
- }
1568
- }
1569
- case 'gates_run':
1570
- return {
1571
- ...review,
1572
- gates: { ok: record.content.ok, checks: record.content.checks },
1573
- }
1574
- case 'leftover_scan':
1575
- return { ...review, leftovers: record.content.hits }
1576
- case 'commit_made':
1577
- return {
1578
- ...review,
1579
- commit: {
1580
- sha: record.content.sha,
1581
- files: record.content.files,
1582
- },
1583
- }
1584
- case 'run_branch_pushed':
1585
- return { ...review, pushed: record.content.sha }
1586
- }
1587
- }
1588
-
1589
- type TicketRecord = Exclude<
1590
- JournalRecord,
1591
- {
1592
- kind:
1593
- | 'run_started'
1594
- | 'intake_read'
1595
- | 'intake_refused'
1596
- | 'nothing_to_do'
1597
- | 'spec_snapshot'
1598
- | 'ticket_snapshot'
1599
- | 'run_branch_created'
1600
- | 'worktrees_removed'
1601
- | 'changeset_written'
1602
- | 'pull_request_opened'
1603
- | 'jev_asked'
1604
- | 'jev_answered'
1605
- | 'jev_failed'
1606
- | 'agent_session'
1607
- | 'agent_session_closed'
1608
- | 'run_stopped'
1609
- | 'limit_wait_started'
1610
- | 'limit_wait_ended'
1611
- | 'usage_line_wait_started'
1612
- | 'usage_line_wait_ended'
1613
- | 'usage_recorded'
1614
- | 'agent_message'
1615
- | 'agent_message_delivered'
1616
- | 'shared_git_changed'
1617
- | 'final_review_started'
1618
- | 'lens_started'
1619
- | 'lens_finished'
1620
- | 'final_review_fixing'
1621
- | 'final_review_stuck'
1622
- | 'final_review_passed'
1623
- | 'final_review_shipped'
1624
- | 'comment_read'
1625
- | 'run_stuck'
1626
- | 'reply_ignored'
1627
- | 'final_review_retried'
1628
- | 'memory_recalled'
1629
- | 'memories_saved'
1630
- | 'learning_skipped'
1631
- | 'memories_reported'
1632
- | 'step_started'
1633
- | 'step_ended'
1634
- | 'run_resumed'
1635
- | 'engine_resumed'
1636
- | 'join_started'
1637
- | 'memory_write_started'
1638
- | 'memory_write_done'
1639
- | 'baseline_reused'
1640
- | 'prepare_made'
1641
- }
1642
- >
1643
-
1644
- type FinishedContent = Extract<
1645
- JournalRecord,
1646
- { kind: 'agent_finished' }
1647
- >['content']
1648
-
1649
- /** A finished agent's assumptions; the learner makes none. */
1650
- const assumptionsOf = (finished: FinishedContent): string[] =>
1651
- 'assumptions' in finished.result ? finished.result.assumptions : []
1652
-
1653
- /**
1654
- * Where a finished agent's result goes in its ticket's progress. A result
1655
- * that answers a fix-loop follow-up counts a round and clears the failed
1656
- * check, so it runs again.
1657
- */
1658
- const resultChange = ({
1659
- progress,
1660
- finished,
1661
- seq,
1662
- }: {
1663
- progress: TicketProgress
1664
- finished: FinishedContent
1665
- seq: number
1666
- }): Partial<TicketProgress> => {
1667
- const fix = progress.review_fix
1668
- const { rejoin } = progress
1669
- switch (finished.role) {
1670
- case 'test-writer':
1671
- // The fresh test-writer after a rebase fixed the clashed tests:
1672
- // no fix round, and the red check stands.
1673
- if (rejoin?.tests_pending) {
1674
- return { rejoin: { ...rejoin, tests_pending: false } }
1675
- }
1676
- if (fix !== null && !fix.tests_answered) {
1677
- return {
1678
- review_fix: {
1679
- ...fix,
1680
- tests_answered: true,
1681
- responses: [
1682
- ...fix.responses,
1683
- ...finished.result.finding_responses,
1684
- ],
1685
- },
1686
- }
1687
- }
1688
- return progress.red_check === null
1689
- ? { test_writer: finished.result }
1690
- : {
1691
- test_writer: finished.result,
1692
- red_check: null,
1693
- red_fix_rounds: progress.red_fix_rounds + 1,
1694
- }
1695
- case 'implementer': {
1696
- const { result } = finished
1697
- // The implementer's answer to the clash message is no fix round.
1698
- if (rejoin?.code_pending) {
1699
- return {
1700
- implementer: result,
1701
- rejoin: { ...rejoin, code_pending: false },
1702
- }
1703
- }
1704
- if (fix !== null && fix.tests_answered && !fix.code_answered) {
1705
- return {
1706
- review_fix: {
1707
- ...fix,
1708
- code_answered: true,
1709
- responses: [
1710
- ...fix.responses,
1711
- ...result.finding_responses,
1712
- ],
1713
- bad_test:
1714
- result.outcome === 'bad_test'
1715
- ? (result.bad_test ?? {
1716
- file: '',
1717
- name: '',
1718
- reason: result.summary,
1719
- })
1720
- : null,
1721
- },
1722
- }
1723
- }
1724
- const bounce: Partial<TicketProgress> =
1725
- result.outcome === 'bad_test'
1726
- ? {
1727
- bad_test_bounces: progress.bad_test_bounces + 1,
1728
- bad_test: result.bad_test ?? {
1729
- file: '',
1730
- name: '',
1731
- reason: result.summary,
1732
- },
1733
- }
1734
- : {}
1735
- const round: Partial<TicketProgress> =
1736
- progress.gates === null
1737
- ? {}
1738
- : {
1739
- gates: null,
1740
- gate_fix_rounds: progress.gate_fix_rounds + 1,
1741
- }
1742
- return { implementer: result, ...round, ...bounce }
1743
- }
1744
- case 'ticket-reviewer':
1745
- return reviewChange({ progress, review: finished.result, seq })
1746
- // Lenses review the run branch, never a ticket.
1747
- default:
1748
- return {}
1749
- }
1750
- }
1751
-
1752
- /**
1753
- * The fixers' "won't fix" answers the new review let go: its ruling was
1754
- * `accepted`, or it gave none and did not list the finding again.
1755
- */
1756
- const declinedBy = ({
1757
- fix,
1758
- review,
1759
- }: {
1760
- fix: Pick<ReviewFix, 'findings' | 'responses'> | null
1761
- review: TicketReviewResult
1762
- }): DeclinedFinding[] => {
1763
- if (fix === null) return []
1764
- const listed = new Set(review.findings.map(({ id }) => id))
1765
- return fix.responses.flatMap(({ finding_id, response, reason }) => {
1766
- if (response !== 'wont_fix') return []
1767
- const finding = fix.findings.find(({ id }) => id === finding_id)
1768
- if (finding === undefined) return []
1769
- const ruling = review.rulings.find(
1770
- (entry) => entry.finding_id === finding_id
1771
- )
1772
- const accepted =
1773
- ruling === undefined
1774
- ? !listed.has(finding_id)
1775
- : ruling.ruling === 'accepted'
1776
- return accepted
1777
- ? [{ finding, reason, ruling: ruling?.reason ?? '' }]
1778
- : []
1779
- })
1780
- }
1781
-
1782
- /**
1783
- * A finished ticket review: counts the round, settles the last round's
1784
- * "won't fix" answers, keeps its nits, and opens a fix round when a finding
1785
- * is a blocker or a should-fix. A new fix round starts its gates, gate fix
1786
- * rounds, and fix commit afresh.
1787
- */
1788
- const reviewChange = ({
1789
- progress,
1790
- review,
1791
- seq,
1792
- }: {
1793
- progress: TicketProgress
1794
- review: TicketReviewResult
1795
- /** The review's record, for the join queue's order once it approves. */
1796
- seq: number
1797
- }): Partial<TicketProgress> => {
1798
- const review_rounds = progress.review_rounds + 1
1799
- const blocking = review.findings.filter(isBlocking)
1800
- const newNits = review.findings.filter(
1801
- (finding) =>
1802
- !isBlocking(finding) &&
1803
- !progress.nits.some(({ id }) => id === finding.id)
1804
- )
1805
- const settled: Partial<TicketProgress> = {
1806
- review,
1807
- review_rounds,
1808
- reviewed_sha: progress.commits.fix ?? progress.commits.green,
1809
- nits: [...progress.nits, ...newNits],
1810
- declined: [
1811
- ...progress.declined,
1812
- ...declinedBy({ fix: progress.review_fix, review }),
1813
- ],
1814
- }
1815
- if (blocking.length === 0) {
1816
- return { ...settled, review_fix: null, approved_seq: seq }
1817
- }
1818
- return {
1819
- ...settled,
1820
- approved_seq: null,
1821
- review_fix: {
1822
- round: review_rounds,
1823
- findings: blocking,
1824
- tests_answered: !blocking.some(({ kind }) => kind === 'test'),
1825
- code_answered: !blocking.some(({ kind }) => kind === 'code'),
1826
- responses: [],
1827
- bad_test: null,
1828
- },
1829
- gates: null,
1830
- gate_fix_rounds: 0,
1831
- leftovers: { ...progress.leftovers, fix: null },
1832
- commits: { ...progress.commits, fix: null },
1833
- commit_files: { ...progress.commit_files, fix: [] },
1834
- }
1835
- }
1836
-
1837
- /** The sessions after a turn: the role's latest, or none if unknown. */
1838
- const sessionsAfter = ({
1839
- sessions,
1840
- role,
1841
- session_id,
1842
- }: {
1843
- sessions: Partial<Record<AgentRole, string>>
1844
- role: AgentRole
1845
- session_id: string | null
1846
- }): Partial<Record<AgentRole, string>> => {
1847
- const rest = omit(sessions, role)
1848
- return session_id === null ? rest : { ...rest, [role]: session_id }
1849
- }
1850
-
1851
- const progressChange = ({
1852
- progress,
1853
- record,
1854
- }: {
1855
- progress: TicketProgress
1856
- record: TicketRecord
1857
- }): Partial<TicketProgress> => {
1858
- switch (record.kind) {
1859
- case 'ticket_worktree_created':
1860
- return { worktree: record.content }
1861
- case 'dependencies_installed':
1862
- return { install: { check: record.content.check } }
1863
- case 'baseline_prepared':
1864
- return { prepare: record.content.check }
1865
- case 'baseline_tests':
1866
- return {
1867
- baseline: record.content,
1868
- baseline_sha: progress.worktree?.base_sha ?? null,
1869
- }
1870
- case 'agent_started':
1871
- return { retried: null, crashed_turn: null }
1872
- case 'agent_finished': {
1873
- const finished = record.content
1874
- return {
1875
- setup_change: setupChangeOf({ finished }),
1876
- agent_failure: null,
1877
- engine_failures: 0,
1878
- sessions: sessionsAfter({
1879
- sessions: progress.sessions,
1880
- role: finished.role,
1881
- session_id: finished.session_id,
1882
- }),
1883
- assumptions: [
1884
- ...progress.assumptions,
1885
- ...assumptionsOf(finished),
1886
- ],
1887
- ...resultChange({ progress, finished, seq: record.seq }),
1888
- }
1889
- }
1890
- case 'agent_failed': {
1891
- const { role, error, failure, session_id } = record.content
1892
- const agent_failure = { role, error, failure, session_id }
1893
- if (failure === 'engine') {
1894
- return {
1895
- agent_failure,
1896
- engine_failures: progress.engine_failures + 1,
1897
- }
1898
- }
1899
- // The engine's side worked this time, so the run of engine
1900
- // failures is over.
1901
- return {
1902
- agent_failure,
1903
- engine_failures: 0,
1904
- failed_tries: {
1905
- ...progress.failed_tries,
1906
- [role]: (progress.failed_tries[role] ?? 0) + 1,
1907
- },
1908
- }
1909
- }
1910
- case 'red_check': {
1911
- const { ok, problems, notes, tests } = record.content
1912
- return { red_check: { ok, problems, notes, output: tests.output } }
1913
- }
1914
- case 'worktree_reset':
1915
- // A fresh test-writer and implementer start over on a clean
1916
- // worktree; the bad test and its count stay.
1917
- return {
1918
- test_writer: null,
1919
- red_check: null,
1920
- red_fix_rounds: 0,
1921
- implementer: null,
1922
- gates: null,
1923
- gate_fix_rounds: 0,
1924
- leftovers: { red: null, green: null, fix: null },
1925
- commits: { ...progress.commits, red: null },
1926
- sessions: omit(progress.sessions, [
1927
- 'test-writer',
1928
- 'implementer',
1929
- ]),
1930
- }
1931
- case 'leftover_scan':
1932
- return {
1933
- leftovers: {
1934
- ...progress.leftovers,
1935
- [record.content.stage]: record.content.hits,
1936
- },
1937
- }
1938
- case 'commit_made':
1939
- return {
1940
- commits: {
1941
- ...progress.commits,
1942
- [record.content.stage]: record.content.sha,
1943
- },
1944
- commit_files: {
1945
- ...progress.commit_files,
1946
- [record.content.stage]: record.content.files,
1947
- },
1948
- }
1949
- case 'gates_run': {
1950
- const { ok, checks } = record.content
1951
- return record.content.target === 'ticket'
1952
- ? { gates: { ok, checks } }
1953
- : { join_gates: { ok, checks } }
1954
- }
1955
- case 'ticket_joined':
1956
- return { joined: record.content }
1957
- case 'ticket_rebased':
1958
- return rebasedChange({ progress, rebased: record.content })
1959
- case 'run_branch_pushed':
1960
- return { pushed: record.content.sha }
1961
- case 'ticket_stuck':
1962
- return { stuck: record.content, stuck_report: null, reply: null }
1963
- case 'stuck_reported':
1964
- return { stuck_report: { comment_id: record.content.comment_id } }
1965
- case 'reply_received': {
1966
- const { word, comment_id } = record.content
1967
- // `stop` is the run's, and `ship` the final review's.
1968
- return word === 'retry' || word === 'skip'
1969
- ? { reply: { word, comment_id } }
1970
- : {}
1971
- }
1972
- case 'ticket_retried':
1973
- return retriedChange({ progress, retried: record.content })
1974
- case 'ticket_skipped':
1975
- return {
1976
- skipped: { because: record.content.because },
1977
- reply: null,
1978
- }
1979
- case 'ticket_already_done':
1980
- return { already_done: { shas: record.content.shas } }
1981
- case 'ticket_closed':
1982
- return { closed: true }
1983
- case 'join_undone':
1984
- return { joined: null, join_gates: null }
1985
- }
1986
- }
1987
-
1988
- /**
1989
- * The final review's report and reply after one of its records: a new
1990
- * stuck is told afresh, and a ship settles the reply.
1991
- */
1992
- const finalRepliesAfter = ({
1993
- state,
1994
- record,
1995
- }: {
1996
- state: RunState
1997
- record: JournalRecord
1998
- }): Pick<RunState, 'final_stuck_report' | 'final_reply'> => {
1999
- switch (record.kind) {
2000
- case 'final_review_stuck':
2001
- return { final_stuck_report: null, final_reply: null }
2002
- case 'final_review_shipped':
2003
- return {
2004
- final_stuck_report: state.final_stuck_report,
2005
- final_reply: null,
2006
- }
2007
- default:
2008
- return {
2009
- final_stuck_report: state.final_stuck_report,
2010
- final_reply: state.final_reply,
2011
- }
2012
- }
2013
- }
2014
-
2015
- /**
2016
- * The stuck final review resumed by `retry`, like a resumed ticket: fresh
2017
- * fixers (no session kept) and fresh counts, and the owner's edits in the
2018
- * run branch's worktree kept. A leftover scan, failed gates, or a step
2019
- * crashes cut off just run again; a failed lens gets a fresh one; otherwise the open fix round
2020
- * starts over as round 1, so the fixes (and the owner's edits) are gated,
2021
- * committed, pushed, and re-reviewed.
2022
- */
2023
- const resumedFinalReview = ({
2024
- review,
2025
- }: {
2026
- review: FinalReviewState
2027
- }): FinalReviewState => {
2028
- const base: FinalReviewState = {
2029
- ...review,
2030
- stuck: null,
2031
- shipped: false,
2032
- sessions: {},
2033
- agent_failures: {},
2034
- failed_tries: {},
2035
- engine_failures: {},
2036
- gate_fix_rounds: 0,
2037
- gates: review.commit === null ? null : review.gates,
2038
- leftovers: review.commit === null ? null : review.leftovers,
2039
- }
2040
- const { fix, stuck } = review
2041
- if (
2042
- fix === null ||
2043
- stuck?.reason === 'leftovers_found' ||
2044
- stuck?.reason === 'gates_failed' ||
2045
- stuck?.reason === 'agent_failed' ||
2046
- stuck?.reason === 'crashed'
2047
- ) {
2048
- return base
2049
- }
2050
- return {
2051
- ...base,
2052
- round: 1,
2053
- fix: {
2054
- ...fix,
2055
- round: 1,
2056
- tests_answered: !fix.findings.some(({ kind }) => kind === 'test'),
2057
- code_answered: !fix.findings.some(({ kind }) => kind === 'code'),
2058
- responses: [],
2059
- bad_test: null,
2060
- },
2061
- fixing_started: false,
2062
- commit: null,
2063
- pushed: null,
2064
- }
2065
- }
2066
-
2067
- /** The spec comments the engine posted, plus this one if it posted one. */
2068
- const engineCommentsAfter = ({
2069
- state,
2070
- comment_id,
2071
- }: {
2072
- state: RunState
2073
- comment_id: number | null
2074
- }): number[] =>
2075
- comment_id === null
2076
- ? state.engine_comments
2077
- : [...state.engine_comments, comment_id]
2078
-
2079
- /** The test setup change an agent asked for in its result, if any. */
2080
- const setupChangeOf = ({
2081
- finished,
2082
- }: {
2083
- finished: FinishedContent
2084
- }): TicketProgress['setup_change'] => {
2085
- if (finished.role !== 'test-writer' && finished.role !== 'implementer') {
2086
- return null
2087
- }
2088
- const { outcome, setup_change } = finished.result
2089
- if (outcome !== 'needs_setup_change') return null
2090
- return {
2091
- role: finished.role,
2092
- file: setup_change?.file ?? '',
2093
- reason: setup_change?.reason ?? finished.result.summary,
2094
- }
2095
- }
2096
-
2097
- type RetriedContent = Extract<
2098
- JournalRecord,
2099
- { kind: 'ticket_retried' }
2100
- >['content']
2101
-
2102
- /**
2103
- * A `retry` of a stuck ticket. `refused` leaves it stuck, waiting for the
2104
- * next reply. `restart` starts it over from scratch (its new snapshot was
2105
- * journaled just before), in its worktree reset to the run branch's tip.
2106
- * `resume` picks up where it stopped (see `resumedProgress`).
2107
- */
2108
- const retriedChange = ({
2109
- progress,
2110
- retried,
2111
- }: {
2112
- progress: TicketProgress
2113
- retried: RetriedContent
2114
- }): Partial<TicketProgress> => {
2115
- switch (retried.mode) {
2116
- case 'refused':
2117
- return { reply: null }
2118
- case 'restart':
2119
- return {
2120
- ...EMPTY_TICKET_PROGRESS,
2121
- worktree:
2122
- progress.worktree === null || retried.base_sha === null
2123
- ? progress.worktree
2124
- : { ...progress.worktree, base_sha: retried.base_sha },
2125
- }
2126
- case 'resume':
2127
- return resumedProgress({ progress })
2128
- }
2129
- }
2130
-
2131
- /**
2132
- * A stuck ticket resumed by `retry`: it picks up where it stopped, with a
2133
- * fresh agent (no session is kept) and fresh counts (fix rounds, failed
2134
- * tries, bad-test bounces, rebases), and keeps whatever the user changed in
2135
- * its worktree. The step that got stuck runs again:
2136
- * - before the red commit, a fresh test-writer writes the tests;
2137
- * - before the green commit, a fresh implementer builds (on top of the run
2138
- * branch after a rebase), then the gates;
2139
- * - in a review fix round, the round starts over as round 1 with fresh
2140
- * fixers, so the user's changes are gated, committed, and re-reviewed;
2141
- * - a leftover scan, a failed install, a failed prepare command at the
2142
- * baseline, a reviewer's failed tries, a failed join, or a step crashes
2143
- * cut off just run again.
2144
- */
2145
- const resumedProgress = ({
2146
- progress,
2147
- }: {
2148
- progress: TicketProgress
2149
- }): Partial<TicketProgress> => {
2150
- const { stuck, commits, review_fix, rejoin } = progress
2151
- const base: Partial<TicketProgress> = {
2152
- stuck: null,
2153
- stuck_report: null,
2154
- reply: null,
2155
- retried: stuck,
2156
- setup_change: null,
2157
- agent_failure: null,
2158
- failed_tries: {},
2159
- engine_failures: 0,
2160
- sessions: {},
2161
- red_fix_rounds: 0,
2162
- gate_fix_rounds: 0,
2163
- bad_test_bounces: 0,
2164
- rejoins: 0,
2165
- install:
2166
- progress.install?.check?.ok === false ? null : progress.install,
2167
- prepare: progress.prepare?.ok === false ? null : progress.prepare,
2168
- leftovers: {
2169
- red: commits.red === null ? null : progress.leftovers.red,
2170
- green: commits.green === null ? null : progress.leftovers.green,
2171
- fix: commits.fix === null ? null : progress.leftovers.fix,
2172
- },
2173
- }
2174
- const reason = stuck?.reason
2175
- const reviewerFailed = progress.agent_failure?.role === 'ticket-reviewer'
2176
- if (
2177
- reason === 'leftovers_found' ||
2178
- reason === 'install_failed' ||
2179
- reason === 'prepare_failed' ||
2180
- reason === 'join_failed' ||
2181
- reason === 'join_gates_failed' ||
2182
- reason === 'crashed' ||
2183
- reviewerFailed
2184
- ) {
2185
- return base
2186
- }
2187
- if (commits.green === null) {
2188
- if (
2189
- rejoin === null &&
2190
- commits.red === null &&
2191
- progress.test_writer !== null
2192
- ) {
2193
- return { ...base, test_writer: null, red_check: null }
2194
- }
2195
- return { ...base, implementer: null, gates: null }
2196
- }
2197
- if (review_fix === null) return base
2198
- return {
2199
- ...base,
2200
- review_rounds: 1,
2201
- review_fix: {
2202
- ...review_fix,
2203
- round: 1,
2204
- tests_answered: !review_fix.findings.some(
2205
- ({ kind }) => kind === 'test'
2206
- ),
2207
- code_answered: !review_fix.findings.some(
2208
- ({ kind }) => kind === 'code'
2209
- ),
2210
- responses: [],
2211
- bad_test: null,
2212
- },
2213
- gates: null,
2214
- leftovers: { ...base.leftovers!, fix: null },
2215
- commits: { ...commits, fix: null },
2216
- commit_files: { ...progress.commit_files, fix: [] },
2217
- }
2218
- }
2219
-
2220
- type RebasedContent = Extract<
2221
- JournalRecord,
2222
- { kind: 'ticket_rebased' }
2223
- >['content']
2224
-
2225
- /**
2226
- * A ticket sent back onto the run branch: its change now sits uncommitted on
2227
- * the run branch's tip. Its tests and code are fixed there, then the gates,
2228
- * one green commit, a fresh review of the new changes (with its own fix
2229
- * rounds), and a new join. The review loop starts over, but its nits and
2230
- * declined findings stay for the PR. The implementer keeps its session; the
2231
- * test-writer and reviewer start fresh. A worktree whose dependencies moved
2232
- * under it is installed again.
2233
- */
2234
- const rebasedChange = ({
2235
- progress,
2236
- rebased,
2237
- }: {
2238
- progress: TicketProgress
2239
- rebased: RebasedContent
2240
- }): Partial<TicketProgress> => {
2241
- const { cause, base_sha, tests, code, reinstall } = rebased
2242
- return {
2243
- worktree:
2244
- progress.worktree === null
2245
- ? null
2246
- : { ...progress.worktree, base_sha },
2247
- install: reinstall ? null : progress.install,
2248
- rejoins: progress.rejoins + 1,
2249
- rejoin: {
2250
- cause,
2251
- base_sha,
2252
- tests,
2253
- code,
2254
- tests_pending: tests.length > 0,
2255
- code_pending: code.length > 0,
2256
- earlier_findings: [
2257
- ...(progress.review?.findings ?? []),
2258
- ...progress.declined.map(({ finding }) => finding),
2259
- ],
2260
- },
2261
- gates: null,
2262
- gate_fix_rounds: 0,
2263
- leftovers: { ...progress.leftovers, green: null, fix: null },
2264
- commits: { ...progress.commits, green: null, fix: null },
2265
- commit_files: { ...progress.commit_files, green: [], fix: [] },
2266
- review: null,
2267
- review_rounds: 0,
2268
- reviewed_sha: null,
2269
- review_fix: null,
2270
- approved_seq: null,
2271
- joined: null,
2272
- join_gates: null,
2273
- pushed: null,
2274
- agent_failure: null,
2275
- engine_failures: 0,
2276
- sessions: omit(progress.sessions, ['test-writer', 'ticket-reviewer']),
2277
- }
2278
- }
2279
-
2280
- /** The final review's scheduler keys: `final`, and `lens:<lens>`. */
2281
- export const isFinalKey = (key: string): boolean =>
2282
- key === 'final' || key.startsWith('lens:')
2283
-
2284
- /** The crash counts without the final review's keys, for its `retry`. */
2285
- const omitFinalKeys = ({ crashes }: { crashes: CrashCounts }): CrashCounts =>
2286
- Object.fromEntries(
2287
- Object.entries(crashes).filter(([key]) => !isFinalKey(key))
2288
- )
2289
-
2290
- const isFixerRole = (role: AgentRole): boolean =>
2291
- role === 'test-writer' || role === 'implementer'
2292
-
2293
- /** A failed turn's session forgotten, when it is this role's. */
2294
- const failureWithoutSession = <
2295
- Failure extends { role: AgentRole; session_id: string | null },
2296
- >({
2297
- failure,
2298
- role,
2299
- }: {
2300
- failure: Failure | null | undefined
2301
- role: AgentRole
2302
- }): Failure | null =>
2303
- failure === null || failure === undefined
2304
- ? null
2305
- : failure.role === role
2306
- ? { ...failure, session_id: null }
2307
- : failure
2308
-
2309
- /**
2310
- * A restarted engine's `run_resumed`: each agent turn a crash cut off is
2311
- * taken again in a fresh session, so its role's session (and a failed
2312
- * turn's session, for a follow-up) is forgotten, on its ticket or in the
2313
- * final review, and the fresh agent is told of the crash. The counts it
2314
- * adds live in `crashes`.
2315
- */
2316
- const resumedAfter = ({
2317
- state,
2318
- record,
2319
- }: {
2320
- state: RunState
2321
- record: Extract<JournalRecord, { kind: 'run_resumed' }>
2322
- }): RunState =>
2323
- record.content.interrupted.reduce((current, { key, ticket, role }) => {
2324
- const parsed = AgentRoleSchema.safeParse(role)
2325
- if (!parsed.success) return current
2326
- const agent = parsed.data
2327
- if (key === 'final') {
2328
- const review = current.final_review
2329
- const failed = failureWithoutSession({
2330
- failure: review.agent_failures[agent],
2331
- role: agent,
2332
- })
2333
- return {
2334
- ...current,
2335
- final_review: {
2336
- ...review,
2337
- sessions: omit(review.sessions, agent),
2338
- agent_failures:
2339
- failed === null
2340
- ? review.agent_failures
2341
- : { ...review.agent_failures, [agent]: failed },
2342
- crashed_turn: isFixerRole(agent)
2343
- ? agent
2344
- : review.crashed_turn,
2345
- },
2346
- }
2347
- }
2348
- if (ticket === null || key !== String(ticket)) return current
2349
- const progress = current.tickets[ticket] ?? EMPTY_TICKET_PROGRESS
2350
- return {
2351
- ...current,
2352
- tickets: {
2353
- ...current.tickets,
2354
- [ticket]: {
2355
- ...progress,
2356
- sessions: omit(progress.sessions, agent),
2357
- agent_failure: failureWithoutSession({
2358
- failure: progress.agent_failure,
2359
- role: agent,
2360
- }),
2361
- crashed_turn: agent,
2362
- },
2363
- },
2364
- }
2365
- }, state)
2366
-
2367
- /**
2368
- * A closed session can take no follow-up: it is forgotten wherever the
2369
- * decision step would follow it up (a role's latest session, or a failed
2370
- * turn's), on its ticket, in the final review, or the learner's, so a fix
2371
- * round that would have gone to it goes to a fresh agent.
2372
- */
2373
- const closedSessionAfter = ({
2374
- state,
2375
- session_id,
2376
- }: {
2377
- state: RunState
2378
- session_id: string
2379
- }): RunState => {
2380
- const forget = <Failure extends { session_id: string | null }>(
2381
- failure: Failure
2382
- ): Failure =>
2383
- failure.session_id === session_id
2384
- ? { ...failure, session_id: null }
2385
- : failure
2386
- const withoutIt = (sessions: Partial<Record<AgentRole, string>>) =>
2387
- omitBy(sessions, (id) => id === session_id)
2388
- const review = state.final_review
2389
- const { learner } = state.memory
2390
- return {
2391
- ...state,
2392
- tickets: mapValues(state.tickets, (progress) => ({
2393
- ...progress,
2394
- sessions: withoutIt(progress.sessions),
2395
- agent_failure:
2396
- progress.agent_failure === null
2397
- ? null
2398
- : forget(progress.agent_failure),
2399
- })),
2400
- final_review: {
2401
- ...review,
2402
- sessions: withoutIt(review.sessions),
2403
- agent_failures: mapValues(review.agent_failures, (failure) =>
2404
- failure === undefined ? failure : forget(failure)
2405
- ),
2406
- },
2407
- memory: {
2408
- ...state.memory,
2409
- learner: {
2410
- ...learner,
2411
- agent_failure:
2412
- learner.agent_failure === null
2413
- ? null
2414
- : forget(learner.agent_failure),
2415
- },
2416
- },
2417
- }
2418
- }
2419
-
2420
- const applyTicketRecord = ({
2421
- state,
2422
- record,
2423
- }: {
2424
- state: RunState
2425
- record: TicketRecord
2426
- }): RunState => {
2427
- if (record.ticket === null) return state
2428
- const progress = state.tickets[record.ticket] ?? EMPTY_TICKET_PROGRESS
2429
- return {
2430
- ...state,
2431
- tickets: {
2432
- ...state.tickets,
2433
- [record.ticket]: {
2434
- ...progress,
2435
- ...progressChange({ progress, record }),
2436
- },
2437
- },
2438
- }
2439
- }
2440
-
2441
- /**
2442
- * A ticket took another ticket's baseline, as it stood when the reuse was
2443
- * journaled, so a replay gives it the same one.
2444
- */
2445
- const reusedBaselineAfter = ({
2446
- state,
2447
- record,
2448
- }: {
2449
- state: RunState
2450
- record: Extract<JournalRecord, { kind: 'baseline_reused' }>
2451
- }): RunState => {
2452
- if (record.ticket === null) return state
2453
- const { from_ticket, base_sha } = record.content
2454
- const baseline = state.tickets[from_ticket]?.baseline ?? null
2455
- if (baseline === null) return state
2456
- const progress = state.tickets[record.ticket] ?? EMPTY_TICKET_PROGRESS
2457
- return {
2458
- ...state,
2459
- tickets: {
2460
- ...state.tickets,
2461
- [record.ticket]: { ...progress, baseline, baseline_sha: base_sha },
2462
- },
2463
- }
2464
- }
2465
-
2466
479
  /**
2467
480
  * Rebuilds a run's state purely from its journal records, in order.
2468
481
  *