@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -0,0 +1,395 @@
1
+ /**
2
+ * One agent turn (`runTurn`): the worktree guard around it, and how it
3
+ * ended, journaled. Also the `launch_agent` and `follow_up_agent` steps,
4
+ * which start a turn.
5
+ */
6
+ import { need, ticketWorktree, type BuildContext } from './build-context'
7
+ import { mayEditTests, type BuildAction } from './decide-build'
8
+ import { doChores } from './engine-chores'
9
+ import { closeSessions } from './session-close'
10
+
11
+ import type { AgentTurn } from '../agents/agent-launcher'
12
+ import {
13
+ parseRoleResult,
14
+ type AgentRole,
15
+ type RoleResult,
16
+ } from '../agents/role-results'
17
+ import { DEFAULT_MODEL } from '../config/engine-config'
18
+ import { describeViolations } from '../guards/after-turn-check'
19
+ import { guardRoleOf } from '../guards/role-rules'
20
+ import { enforceAfterTurn, snapshotWorktree } from '../guards/worktree-state'
21
+ import { launchPick, type ModelReason } from '../jev/model-pick'
22
+ import type { AgentFailure, JournalRecord } from '../journal/journal-record'
23
+ import type { ReplayedWorktree } from '../journal/replay'
24
+ import { sessionSignal } from '../limits/plan-signals'
25
+ import { createAgentMessaging } from '../messages/agent-messaging'
26
+
27
+ const failTurn = ({
28
+ context,
29
+ ticket,
30
+ role,
31
+ error,
32
+ failure,
33
+ session_id,
34
+ session_gone,
35
+ out_of_turns,
36
+ }: {
37
+ context: BuildContext
38
+ ticket: number | null
39
+ role: AgentRole
40
+ error: string
41
+ failure: AgentFailure
42
+ session_id: string | null
43
+ session_gone: boolean
44
+ out_of_turns: boolean
45
+ }) => {
46
+ context.journal.append({
47
+ kind: 'agent_failed',
48
+ ticket,
49
+ role,
50
+ content: {
51
+ role,
52
+ error,
53
+ failure,
54
+ session_id,
55
+ ...(session_gone ? { session_gone } : {}),
56
+ ...(out_of_turns ? { out_of_turns } : {}),
57
+ },
58
+ })
59
+ }
60
+
61
+ /**
62
+ * Runs one agent turn, a launch or a follow-up, and journals how it ended.
63
+ * Before the turn the engine snapshots the worktree; after it, the
64
+ * after-turn check undoes and reports anything the role may not change, for
65
+ * every launcher alike; what others changed in the shared `.git` meanwhile
66
+ * is journaled as `shared_git_changed`. Then the result is judged by its
67
+ * structured output. Each failed turn is journaled once as `agent_failed`
68
+ * with how it failed; the decision step picks what happens next. A writer's
69
+ * result then gets its chores done (`doChores`, #618); one the engine could
70
+ * not do fails the turn as a `chore` failure. A launcher stop journals
71
+ * `run_stopped` and ends the run. A turn the plan cut off (a rejected limit,
72
+ * overage, a billing error) journals only its session. Both close the
73
+ * turn's session first, since no follow-up can reach it.
74
+ *
75
+ * @param worktree - Where the agent works: its ticket's worktree, or the run
76
+ * branch's for the final review.
77
+ * @param ticket - The ticket its records name, `null` for the final review.
78
+ * @returns The role's result when the agent finished, else `null`.
79
+ */
80
+ export const runTurn = async ({
81
+ context,
82
+ worktree,
83
+ ticket,
84
+ role,
85
+ may_edit_tests,
86
+ start,
87
+ }: {
88
+ context: BuildContext
89
+ worktree: ReplayedWorktree
90
+ ticket: number | null
91
+ role: AgentRole
92
+ may_edit_tests: boolean
93
+ start: (cwd: string) => Promise<AgentTurn>
94
+ }): Promise<RoleResult | null> => {
95
+ const { path, branch } = worktree
96
+ const before = await snapshotWorktree({
97
+ cwd: path,
98
+ branch,
99
+ config: context.config,
100
+ })
101
+ const turn = await start(path)
102
+ if (turn.session !== undefined) {
103
+ context.journal.append({
104
+ kind: 'agent_session',
105
+ ticket,
106
+ role,
107
+ content: { role, session: turn.session },
108
+ })
109
+ }
110
+ const guard_role = guardRoleOf({ role })
111
+ const { violations, outside } = await enforceAfterTurn({
112
+ cwd: path,
113
+ branch,
114
+ role: guard_role,
115
+ may_edit_tests,
116
+ config: context.config,
117
+ before,
118
+ leave_out: context.state.prepare_made,
119
+ })
120
+ // Others' changes to the shared .git: noted, never blamed or undone.
121
+ if (outside.length > 0) {
122
+ context.journal.append({
123
+ kind: 'shared_git_changed',
124
+ ticket,
125
+ role,
126
+ content: { role, changes: outside },
127
+ })
128
+ }
129
+ // The plan cut the turn off. Its session, journaled above, holds why,
130
+ // and the decision step reads it from there: a limit wait or a billing
131
+ // stop. The turn uses up no try, and its step is taken again. With no
132
+ // sign in the session, the engine can't tell which, so it stops.
133
+ const unexplained =
134
+ !turn.ok &&
135
+ turn.failure === 'plan' &&
136
+ (turn.session === undefined ||
137
+ sessionSignal({ session: turn.session }).kind === 'ok')
138
+ // Either way no follow-up reaches the session: close it now.
139
+ const cut = !turn.ok && (turn.failure === 'plan' || turn.failure === 'stop')
140
+ if (cut && turn.session_id !== undefined) {
141
+ await closeSessions({
142
+ journal: context.journal,
143
+ launcher: context.launcher,
144
+ sessions: [{ session_id: turn.session_id, ticket, role }],
145
+ })
146
+ }
147
+ if (!turn.ok && turn.failure === 'plan' && !unexplained) return null
148
+ if (!turn.ok && (turn.failure === 'stop' || unexplained)) {
149
+ context.journal.append({
150
+ kind: 'run_stopped',
151
+ ticket,
152
+ role,
153
+ content: { reason: turn.error, role, billing: false },
154
+ })
155
+ throw new Error(`Run stopped: ${turn.error}`)
156
+ }
157
+ const session_id = turn.session_id ?? null
158
+ const fail = ({
159
+ failure,
160
+ error,
161
+ session_gone = false,
162
+ out_of_turns = false,
163
+ }: {
164
+ failure: AgentFailure
165
+ error: string
166
+ session_gone?: boolean
167
+ out_of_turns?: boolean
168
+ }) => {
169
+ failTurn({
170
+ context,
171
+ ticket,
172
+ role,
173
+ error,
174
+ failure,
175
+ session_id,
176
+ session_gone,
177
+ out_of_turns,
178
+ })
179
+ return null
180
+ }
181
+ if (violations.length > 0) {
182
+ return fail({
183
+ failure: 'guard',
184
+ error: describeViolations({ role: guard_role, violations }),
185
+ })
186
+ }
187
+ if (!turn.ok) {
188
+ return fail({
189
+ failure: turn.failure === 'engine' ? 'engine' : 'agent',
190
+ error: turn.error,
191
+ session_gone: turn.session_gone === true,
192
+ out_of_turns: turn.out_of_turns === true,
193
+ })
194
+ }
195
+ if (
196
+ turn.structured_output === undefined ||
197
+ turn.structured_output === null
198
+ ) {
199
+ return fail({
200
+ failure: 'result',
201
+ error: `The ${role} finished with no structured output.`,
202
+ })
203
+ }
204
+ const checked = parseRoleResult({ role, output: turn.structured_output })
205
+ if (!checked.ok) return fail({ failure: 'result', error: checked.error })
206
+ // The chores the writer asked for (#618), before any check or commit.
207
+ const problems = await doChores({
208
+ context,
209
+ cwd: path,
210
+ ticket,
211
+ result: checked.value,
212
+ guard: { role: guard_role, may_edit_tests },
213
+ })
214
+ if (problems.length > 0) {
215
+ return fail({
216
+ failure: 'chore',
217
+ error: problems.map((line) => `- ${line}`).join('\n'),
218
+ })
219
+ }
220
+ context.journal.append({
221
+ kind: 'agent_finished',
222
+ ticket,
223
+ role,
224
+ content: { ...checked.value, session_id },
225
+ })
226
+ return checked.value
227
+ }
228
+
229
+ /** The session a record names, if any. */
230
+ const sessionOf = (record: JournalRecord): string | null => {
231
+ switch (record.kind) {
232
+ case 'agent_session':
233
+ return record.content.session.session_id
234
+ case 'agent_finished':
235
+ case 'agent_failed':
236
+ return record.content.session_id
237
+ default:
238
+ return null
239
+ }
240
+ }
241
+
242
+ /** The model a session launched on, and why (`null` before #603). */
243
+ export type SessionModel = { model: string; model_reason: ModelReason | null }
244
+
245
+ /**
246
+ * The model a session launched on, and why, from the journal: the
247
+ * `agent_started` of the launch the session's first record came after (same
248
+ * ticket and role), or of an earlier follow-up to it. `null` if the journal
249
+ * names none, as in journals from before #600; every agent ran Opus then.
250
+ *
251
+ * @example
252
+ * sessionModel({ records: journal.read(), session_id }) // { model: 'opus', model_reason: 'locked' }
253
+ */
254
+ export const sessionModel = ({
255
+ records,
256
+ session_id,
257
+ }: {
258
+ records: JournalRecord[]
259
+ session_id: string
260
+ }): SessionModel | null => {
261
+ const followed = records.findLast(
262
+ (record) =>
263
+ record.kind === 'agent_started' &&
264
+ record.content.follow_up_of === session_id &&
265
+ record.content.model !== null
266
+ )
267
+ const first = records.findIndex(
268
+ (record) => sessionOf(record) === session_id
269
+ )
270
+ const { ticket, role } = records[first] ?? {}
271
+ const started =
272
+ followed ??
273
+ (first === -1
274
+ ? undefined
275
+ : records
276
+ .slice(0, first)
277
+ .findLast(
278
+ (record) =>
279
+ record.kind === 'agent_started' &&
280
+ record.ticket === ticket &&
281
+ record.role === role &&
282
+ record.content.follow_up_of === null
283
+ ))
284
+ if (started?.kind !== 'agent_started') return null
285
+ const { model, model_reason } = started.content
286
+ return model === null ? null : { model, model_reason }
287
+ }
288
+
289
+ /** A follow-up's model: its session's, else Opus, as before #600. */
290
+ export const followUpModel = ({
291
+ records,
292
+ session_id,
293
+ }: {
294
+ records: JournalRecord[]
295
+ session_id: string
296
+ }): SessionModel =>
297
+ sessionModel({ records, session_id }) ?? {
298
+ model: DEFAULT_MODEL,
299
+ model_reason: null,
300
+ }
301
+
302
+ export const launchAgent = async ({
303
+ context,
304
+ action,
305
+ }: {
306
+ context: BuildContext
307
+ action: Extract<BuildAction, { type: 'launch_agent' }>
308
+ }) => {
309
+ const { ticket, role, prompt, may_edit_tests } = action
310
+ // The decision model's answer, journaled just before this step, picks
311
+ // a routed role's model; a reviewer is locked on Opus. A redo keeps
312
+ // the model its first try journaled.
313
+ const { model, model_reason } = launchPick({
314
+ records: context.journal.read(),
315
+ state: context.state,
316
+ config: context.config,
317
+ ticket,
318
+ role,
319
+ step: context.step,
320
+ })
321
+ context.journal.append({
322
+ kind: 'agent_started',
323
+ ticket,
324
+ role,
325
+ content: { role, prompt, follow_up_of: null, model, model_reason },
326
+ })
327
+ await runTurn({
328
+ context,
329
+ worktree: ticketWorktree({ state: context.state, ticket }),
330
+ ticket,
331
+ role,
332
+ may_edit_tests,
333
+ start: (cwd) =>
334
+ context.launcher.launch({
335
+ role,
336
+ ticket,
337
+ prompt,
338
+ cwd,
339
+ may_edit_tests,
340
+ config: context.config,
341
+ messaging: createAgentMessaging({
342
+ journal: context.journal,
343
+ ticket,
344
+ role,
345
+ }),
346
+ model,
347
+ }),
348
+ })
349
+ }
350
+
351
+ export const followUpAgent = async ({
352
+ context,
353
+ action,
354
+ }: {
355
+ context: BuildContext
356
+ action: Extract<BuildAction, { type: 'follow_up_agent' }>
357
+ }) => {
358
+ const { ticket, role, session_id, message } = action
359
+ const snapshot = need({
360
+ value: context.state.snapshot?.tickets[ticket],
361
+ what: `snapshot of #${ticket}`,
362
+ })
363
+ const { model, model_reason } = followUpModel({
364
+ records: context.journal.read(),
365
+ session_id,
366
+ })
367
+ context.journal.append({
368
+ kind: 'agent_started',
369
+ ticket,
370
+ role,
371
+ content: {
372
+ role,
373
+ prompt: message,
374
+ follow_up_of: session_id,
375
+ model,
376
+ model_reason,
377
+ },
378
+ })
379
+ await runTurn({
380
+ context,
381
+ worktree: ticketWorktree({ state: context.state, ticket }),
382
+ ticket,
383
+ role,
384
+ may_edit_tests: mayEditTests({ role, ticket: snapshot }),
385
+ start: (cwd) =>
386
+ context.launcher.followUp({
387
+ session_id,
388
+ role,
389
+ ticket,
390
+ message,
391
+ cwd,
392
+ config: context.config,
393
+ }),
394
+ })
395
+ }
@@ -3,7 +3,7 @@ import type {
3
3
  DoneCommit,
4
4
  TestWriterResult,
5
5
  } from '../agents/role-results'
6
- import type { TicketSnapshot } from '../intake/intake-schemas'
6
+ import type { LeftOutTicket, TicketSnapshot } from '../intake/intake-schemas'
7
7
 
8
8
  /**
9
9
  * A ticket whose work is already on the base branch (#484): its
@@ -30,13 +30,56 @@ export type AlreadyDoneAction =
30
30
  /**
31
31
  * End the run with nothing to do: at intake (`already_done` is empty),
32
32
  * or once every ticket's work was already done and those are closed.
33
+ * At intake, `left_out` names the open tickets left for a person (#499).
33
34
  */
34
35
  | {
35
36
  type: 'finish_nothing_to_do'
36
37
  closed_tickets: number[]
37
38
  already_done: number[]
39
+ left_out?: LeftOutTicket[]
38
40
  }
39
41
 
42
+ /**
43
+ * Check a test-writer's `already_done` evidence (#495) in the ticket's
44
+ * worktree before the ticket counts as done: each commit in `done_by` is on
45
+ * `base_sha` (the commit the worktree started from), every criterion names
46
+ * a test, and each named test exists and passes there. The engine journals
47
+ * the check as `already_done_checked`.
48
+ */
49
+ export type CheckAlreadyDoneAction = {
50
+ type: 'check_already_done'
51
+ ticket: number
52
+ base_sha: string
53
+ criteria_ids: string[]
54
+ done_by: DoneCommit[]
55
+ criteria: CriterionTests[]
56
+ }
57
+
58
+ /**
59
+ * The step that checks a test-writer's `already_done` evidence (#495), on
60
+ * the commit the ticket's worktree started from.
61
+ *
62
+ * @example
63
+ * checkAlreadyDoneStep({ ticket, base_sha: 'b0', result })
64
+ * // { type: 'check_already_done', ticket: 11, base_sha: 'b0', criteria_ids: ['AC1'], done_by, criteria }
65
+ */
66
+ export const checkAlreadyDoneStep = ({
67
+ ticket,
68
+ base_sha,
69
+ result,
70
+ }: {
71
+ ticket: TicketSnapshot
72
+ base_sha: string
73
+ result: TestWriterResult
74
+ }): CheckAlreadyDoneAction => ({
75
+ type: 'check_already_done',
76
+ ticket: ticket.number,
77
+ base_sha,
78
+ criteria_ids: ticket.criteria.map(({ id }) => id),
79
+ done_by: result.done_by,
80
+ criteria: result.criteria,
81
+ })
82
+
40
83
  /** How many characters of a sha the engine shows. */
41
84
  const SHORT_SHA = 7
42
85
 
@@ -0,0 +1,214 @@
1
+ /**
2
+ * The build step's actions (`BuildAction`), and what every ticket step
3
+ * shares: its arguments, a stuck action, and whether a ticket is a refactor.
4
+ */
5
+
6
+ import type { AlreadyDoneAction, CheckAlreadyDoneAction } from './already-done'
7
+ import type { FinalReviewAction } from './decide-final-review'
8
+ import type { StuckAction } from './decide-stuck'
9
+
10
+ import type { AgentRole, BadTest, CriterionTests } from '../agents/role-results'
11
+ import type { TicketSnapshot } from '../intake/intake-schemas'
12
+ import type {
13
+ ChangesetBump,
14
+ CommitStage,
15
+ GateTarget,
16
+ RejoinCause,
17
+ StuckReason,
18
+ } from '../journal/journal-record'
19
+ import type {
20
+ ReplayedRunNote,
21
+ ReplayedSnapshot,
22
+ TicketProgress,
23
+ } from '../journal/replay'
24
+ import { REFACTOR_LABEL } from '../tracker/tracker'
25
+
26
+ /** The next build step for a run whose intake passed. */
27
+ export type BuildAction =
28
+ /** Make the run branch, in its own worktree, from the base branch. */
29
+ | { type: 'create_run_branch'; spec_number: number; base_branch: string }
30
+ /** Make the ticket's worktree on a new branch from the run branch. */
31
+ | { type: 'create_ticket_worktree'; ticket: number; run_branch: string }
32
+ /**
33
+ * Install the dependencies from the lockfile, without changing it, in a
34
+ * new ticket worktree or the run branch's checkout (`ticket` is `null`),
35
+ * before any agent or gate runs there.
36
+ */
37
+ | {
38
+ type: 'install_dependencies'
39
+ target: GateTarget
40
+ ticket: number | null
41
+ }
42
+ /** Run the tests before any agent works, to know the old tests. */
43
+ | { type: 'run_baseline_tests'; ticket: number }
44
+ /**
45
+ * Take `from_ticket`'s baseline instead of running the tests again: both
46
+ * worktrees start from the same run-branch commit, `base_sha`.
47
+ */
48
+ | {
49
+ type: 'reuse_baseline_tests'
50
+ ticket: number
51
+ from_ticket: number
52
+ base_sha: string
53
+ }
54
+ /**
55
+ * Start a fresh agent session in the ticket's worktree with this prompt.
56
+ * `may_edit_tests` is true for the test-writer and for a refactor
57
+ * ticket's implementer; the launcher's guards enforce it.
58
+ */
59
+ | {
60
+ type: 'launch_agent'
61
+ ticket: number
62
+ role: AgentRole
63
+ prompt: string
64
+ may_edit_tests: boolean
65
+ }
66
+ /**
67
+ * Send a follow-up message, such as a failed check's output, to the agent
68
+ * session that is still open, in a fix loop.
69
+ */
70
+ | {
71
+ type: 'follow_up_agent'
72
+ ticket: number
73
+ role: AgentRole
74
+ session_id: string
75
+ message: string
76
+ }
77
+ /**
78
+ * Throw away every uncommitted change in the ticket's worktree, so a
79
+ * fresh test-writer starts clean after a bad test. With a `bad_test`, the
80
+ * engine itself found the test checks nothing (#581); without one, the
81
+ * implementer sent it back.
82
+ */
83
+ | { type: 'reset_ticket_worktree'; ticket: number; bad_test?: BadTest }
84
+ /**
85
+ * Prove every criterion has a test, new tests fail, old tests pass. A
86
+ * lone rerun (`exclusive`, #544) runs its tests alone.
87
+ */
88
+ | {
89
+ type: 'run_red_check'
90
+ ticket: number
91
+ criteria_ids: string[]
92
+ mapping: CriterionTests[]
93
+ exclusive?: true
94
+ }
95
+ /** Scan for leftovers, then commit everything in the worktree. */
96
+ | {
97
+ type: 'commit_ticket'
98
+ ticket: number
99
+ stage: CommitStage
100
+ message: string
101
+ }
102
+ /**
103
+ * Run the engine config's gates in the ticket worktree or on the run
104
+ * branch. A lone rerun (`exclusive`, #544) runs its tests alone.
105
+ */
106
+ | {
107
+ type: 'run_gates'
108
+ ticket: number
109
+ target: GateTarget
110
+ exclusive?: true
111
+ }
112
+ /** Replay the approved ticket's commits onto the run branch. */
113
+ | { type: 'join_run_branch'; ticket: number }
114
+ /**
115
+ * Put a joined ticket's whole change back on top of the run branch, as
116
+ * uncommitted changes in its worktree, after a clash or failed gates
117
+ * after joining. With `undo_first_sha`, first undo the join on the run
118
+ * branch, back to before that commit.
119
+ */
120
+ | {
121
+ type: 'rebase_ticket'
122
+ ticket: number
123
+ cause: RejoinCause
124
+ undo_first_sha: string | null
125
+ }
126
+ /**
127
+ * After a rebase, send the test the implementer called bad back to the
128
+ * ticket's test-writer to update (#489), keeping the implementer's code.
129
+ * The engine looks up what joined the run branch between `from_sha` and
130
+ * `base_sha` and journals it all as `tests_sent_back`.
131
+ */
132
+ | {
133
+ type: 'send_tests_back'
134
+ ticket: number
135
+ round: number
136
+ bad_test: BadTest
137
+ from_sha: string
138
+ base_sha: string
139
+ }
140
+ /** Remove these git worktrees at the end of the run; branches stay. */
141
+ | { type: 'remove_worktrees'; paths: string[] }
142
+ /** No ticket can move, and not every ticket has joined. */
143
+ | { type: 'invalid_journal'; reason: string }
144
+ /** Push the run branch to `origin`. */
145
+ | { type: 'push_run_branch'; ticket: number; branch: string }
146
+ /** The engine can't safely pick this ticket's next step by itself. */
147
+ | {
148
+ type: 'mark_stuck'
149
+ ticket: number
150
+ reason: StuckReason
151
+ detail: string
152
+ }
153
+ /**
154
+ * In a repo with changesets, commit the run's one changeset to the run
155
+ * branch and push it, before the PR (#461).
156
+ */
157
+ | {
158
+ type: 'write_changeset'
159
+ branch: string
160
+ bump: ChangesetBump
161
+ summary: string
162
+ message: string
163
+ }
164
+ /** Open the run's one pull request from the run branch. */
165
+ | {
166
+ type: 'open_pull_request'
167
+ head: string
168
+ base: string
169
+ title: string
170
+ body: string
171
+ }
172
+ /** Every ticket joined (or was skipped) and the PR is open. */
173
+ | {
174
+ type: 'done'
175
+ outcome: 'pr_opened'
176
+ pull_request: { number: number; url: string }
177
+ }
178
+ /** The final review of the whole run branch (`decide-final-review.ts`). */
179
+ | FinalReviewAction
180
+ /** The owner replied `stop`: the run ends without a PR; its branch stays. */
181
+ | { type: 'done'; outcome: 'stopped_by_user' }
182
+ /** Every ticket was skipped, so there is nothing to open a PR for. */
183
+ | { type: 'done'; outcome: 'all_skipped' }
184
+ /** Telling the owner about stuck work, and acting on their replies. */
185
+ | StuckAction
186
+ /** A ticket whose work was already on the base branch (#484). */
187
+ | AlreadyDoneAction
188
+ /** Check a test-writer's evidence that the work is already done (#495). */
189
+ | CheckAlreadyDoneAction
190
+
191
+ /** Whether a ticket is a refactor ticket: it skips the test-writer and red check. */
192
+ export const isRefactorTicket = ({
193
+ ticket,
194
+ }: {
195
+ ticket: TicketSnapshot
196
+ }): boolean => ticket.labels.includes(REFACTOR_LABEL)
197
+
198
+ export type StepArgs = {
199
+ snapshot: ReplayedSnapshot
200
+ ticket: TicketSnapshot
201
+ progress: TicketProgress
202
+ /** The run notes a fresh agent gets (`newestRunNotes`). */
203
+ run_notes: ReplayedRunNote[]
204
+ }
205
+
206
+ export const stuck = ({
207
+ ticket,
208
+ reason,
209
+ detail,
210
+ }: {
211
+ ticket: number
212
+ reason: StuckReason
213
+ detail: string
214
+ }): BuildAction => ({ type: 'mark_stuck', ticket, reason, detail })