@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,135 @@
1
+ /**
2
+ * What a build step's executor works with (`BuildContext`, `buildContext`),
3
+ * and the small lookups every step shares: a journaled value, a ticket's
4
+ * worktree, a report file, and the test files in a worktree.
5
+ */
6
+ import { mkdir } from 'node:fs/promises'
7
+ import { basename, dirname, join } from 'node:path'
8
+
9
+ import type { AgentLauncher } from '../agents/agent-launcher'
10
+ import type { EngineConfig } from '../config/engine-config'
11
+ import { testFilesAmong } from '../gates/test-runner'
12
+ import type { GitAdapter } from '../git/git-adapter'
13
+ import type { Journal } from '../journal/journal'
14
+ import {
15
+ replayRun,
16
+ type ReplayedWorktree,
17
+ type RunState,
18
+ } from '../journal/replay'
19
+ import type { CommentStep } from '../tracker/post-comment-once'
20
+ import type { Tracker } from '../tracker/tracker'
21
+
22
+ /** What the engine needs, beyond the journal and tracker, to build tickets. */
23
+ export type BuildDeps = {
24
+ git: GitAdapter
25
+ launcher: AgentLauncher
26
+ }
27
+
28
+ /** What a build step's executor works with. */
29
+ export type BuildContext = BuildDeps & {
30
+ journal: Journal
31
+ tracker: Tracker
32
+ state: RunState
33
+ config: EngineConfig
34
+ /** The run's folder, next to its journal and outside git. */
35
+ run_dir: string
36
+ /**
37
+ * Which try of its step this is: a redo after a crash adopts what its
38
+ * first try left behind.
39
+ */
40
+ step: CommentStep
41
+ }
42
+
43
+ /** A journaled value, or a clear error that the journal has none yet. */
44
+ export const need = <T>({
45
+ value,
46
+ what,
47
+ }: {
48
+ value: T | null | undefined
49
+ what: string
50
+ }): T => {
51
+ if (value === null || value === undefined) {
52
+ throw new Error(`The journal has no ${what} yet.`)
53
+ }
54
+ return value
55
+ }
56
+
57
+ export const ticketWorktree = ({
58
+ state,
59
+ ticket,
60
+ }: {
61
+ state: RunState
62
+ ticket: number
63
+ }): ReplayedWorktree =>
64
+ need({
65
+ value: state.tickets[ticket]?.worktree,
66
+ what: `worktree for #${ticket}`,
67
+ })
68
+
69
+ /** A fresh report file outside the worktree, so it is never a leftover. */
70
+ export const reportFile = async ({
71
+ context,
72
+ ticket,
73
+ label,
74
+ }: {
75
+ context: BuildContext
76
+ /** `null` for the final review. */
77
+ ticket: number | null
78
+ label: string
79
+ }): Promise<string> => {
80
+ const folder = join(context.run_dir, 'reports')
81
+ await mkdir(folder, { recursive: true })
82
+ return join(
83
+ folder,
84
+ `${context.state.last_seq + 1}-${ticket ?? 'final'}-${label}.xml`
85
+ )
86
+ }
87
+
88
+ export const testFilesIn = async ({
89
+ context,
90
+ cwd,
91
+ }: {
92
+ context: BuildContext
93
+ cwd: string
94
+ }): Promise<string[]> =>
95
+ testFilesAmong({
96
+ files: await context.git.listFiles({ cwd }),
97
+ test_file_patterns: context.config.test_file_patterns,
98
+ })
99
+
100
+ /**
101
+ * What a build step's executor works with, from the journal as it is now.
102
+ *
103
+ * @example
104
+ * const context = buildContext({ journal, tracker, git, launcher })
105
+ */
106
+ export const buildContext = ({
107
+ journal,
108
+ tracker,
109
+ git,
110
+ launcher,
111
+ step,
112
+ }: BuildDeps & {
113
+ journal: Journal
114
+ tracker: Tracker
115
+ /** Left out, a first try, numbered after the journal's last record. */
116
+ step?: CommentStep
117
+ }): BuildContext => {
118
+ const records = journal.read()
119
+ const state = replayRun({ records })
120
+ const run_dir = dirname(journal.file)
121
+ return {
122
+ git,
123
+ launcher,
124
+ journal,
125
+ tracker,
126
+ state,
127
+ config: need({ value: state.config, what: 'engine config' }),
128
+ run_dir,
129
+ step: step ?? {
130
+ run_id: basename(run_dir),
131
+ first_seq: (records.at(-1)?.seq ?? 0) + 1,
132
+ redo: false,
133
+ },
134
+ }
135
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * A fresh agent's launch in the build: the run notes it gets, whether it may
3
+ * edit tests, and the sections its prompt adds.
4
+ */
5
+
6
+ import {
7
+ isRefactorTicket,
8
+ type BuildAction,
9
+ type StepArgs,
10
+ } from './build-action'
11
+ import {
12
+ CRASH_SECTION,
13
+ followUpSection,
14
+ testsUpdatedMessage,
15
+ } from './fix-loop-text'
16
+ import { handoffSection, reviewFixSection, reviewSections } from './review-text'
17
+ import { retrySection } from './stuck-text'
18
+
19
+ import {
20
+ CHANGESETS_LENS_SECTION,
21
+ CHANGESETS_SECTION,
22
+ rejoinSection,
23
+ rolePrompt,
24
+ testUpdateSection,
25
+ } from '../agents/role-prompts'
26
+ import type { AgentRole } from '../agents/role-results'
27
+ import type { TicketSnapshot } from '../intake/intake-schemas'
28
+ import {
29
+ handoffRole,
30
+ type ReplayedRunNote,
31
+ type TicketProgress,
32
+ } from '../journal/replay'
33
+
34
+ /**
35
+ * How many run notes a fresh agent is handed: the newest ones in the run,
36
+ * whatever ticket or role wrote them.
37
+ */
38
+ export const MAX_RUN_NOTES = 10
39
+
40
+ /**
41
+ * The run notes a fresh agent gets: the `MAX_RUN_NOTES` newest, oldest
42
+ * first. The same text written twice counts once, where it was last written.
43
+ *
44
+ * @example
45
+ * newestRunNotes({ run_notes: state.run_notes }) // at most 10, oldest first
46
+ */
47
+ export const newestRunNotes = ({
48
+ run_notes,
49
+ }: {
50
+ run_notes: ReplayedRunNote[]
51
+ }): ReplayedRunNote[] => {
52
+ const seen = new Set<string>()
53
+ const newest: ReplayedRunNote[] = []
54
+ for (const entry of [...run_notes].reverse()) {
55
+ if (newest.length === MAX_RUN_NOTES) break
56
+ if (seen.has(entry.note)) continue
57
+ seen.add(entry.note)
58
+ newest.push(entry)
59
+ }
60
+ return newest.reverse()
61
+ }
62
+
63
+ /**
64
+ * Whether an agent of this role may edit test files on this ticket: only
65
+ * the test-writer, and a refactor ticket's implementer (who may follow
66
+ * renames into tests). The engine hands it to the launcher's guards and
67
+ * uses it for its own after-turn check, on launches and follow-ups alike.
68
+ *
69
+ * @example
70
+ * mayEditTests({ role: 'implementer', ticket }) // true only on a refactor ticket
71
+ */
72
+ export const mayEditTests = ({
73
+ role,
74
+ ticket,
75
+ }: {
76
+ role: AgentRole
77
+ ticket: TicketSnapshot
78
+ }): boolean =>
79
+ role === 'test-writer' ||
80
+ (role === 'implementer' && isRefactorTicket({ ticket }))
81
+
82
+ /**
83
+ * The sections a launch adds: the review's (see `reviewPromptSections`),
84
+ * and, for a test-writer or implementer fixing a ticket on top of the run
85
+ * branch, the files that clashed. The reviewer's re-review after a rebase
86
+ * is the review's own. The first agent after a `retry` resumed a stuck
87
+ * ticket is also told why it got stuck, and the agent taking a turn a
88
+ * crash cut off is told of the crash.
89
+ */
90
+ const promptSections = ({
91
+ role,
92
+ progress,
93
+ }: {
94
+ role: AgentRole
95
+ progress: TicketProgress
96
+ }): string[] => {
97
+ const sections = buildPromptSections({ role, progress })
98
+ const { retried, crashed_turn } = progress
99
+ return [
100
+ ...sections,
101
+ ...(retried === null ? [] : [retrySection({ retried })]),
102
+ ...(crashed_turn === role ? [CRASH_SECTION] : []),
103
+ ]
104
+ }
105
+
106
+ const buildPromptSections = ({
107
+ role,
108
+ progress,
109
+ }: {
110
+ role: AgentRole
111
+ progress: TicketProgress
112
+ }): string[] => {
113
+ const review = reviewPromptSections({ role, progress })
114
+ const { rejoin } = progress
115
+ if (rejoin === null || role === 'ticket-reviewer') return review
116
+ // A review fix round after the rejoin has its own findings.
117
+ if (progress.commits.green !== null) return review
118
+ const section = rejoinSection({ role, rejoin })
119
+ return [
120
+ ...review,
121
+ ...(section === null ? [] : [section]),
122
+ ...testUpdateSections({ role, progress }),
123
+ ]
124
+ }
125
+
126
+ /**
127
+ * The sections a launch adds while a test update is open (#489): the
128
+ * test-writer's task, then, once it answered, the implementer's news.
129
+ */
130
+ const testUpdateSections = ({
131
+ role,
132
+ progress,
133
+ }: {
134
+ role: AgentRole
135
+ progress: TicketProgress
136
+ }): string[] => {
137
+ const update = progress.test_update
138
+ if (update === null) return []
139
+ if (role === 'test-writer' && update.result === null) {
140
+ return [testUpdateSection({ since: update, bad_test: update.bad_test })]
141
+ }
142
+ if (role === 'implementer' && update.result !== null) {
143
+ return [
144
+ testsUpdatedMessage({
145
+ bad_test: update.bad_test,
146
+ summary: update.result.summary,
147
+ }),
148
+ ]
149
+ }
150
+ return []
151
+ }
152
+
153
+ /**
154
+ * The sections a launch adds for the ticket review: the reviewer's diff and
155
+ * gate results (a re-review's new changes and earlier findings), or a
156
+ * review fixer's findings while a review fix round is open.
157
+ */
158
+ const reviewPromptSections = ({
159
+ role,
160
+ progress,
161
+ }: {
162
+ role: AgentRole
163
+ progress: TicketProgress
164
+ }): string[] => {
165
+ const fix = progress.review_fix
166
+ if (role === 'ticket-reviewer') {
167
+ return reviewSections({
168
+ progress,
169
+ base_sha: progress.worktree?.base_sha ?? 'HEAD',
170
+ })
171
+ }
172
+ if (fix === null || progress.commits.green === null) return []
173
+ if (role === 'test-writer' && !fix.tests_answered) {
174
+ return [reviewFixSection({ fix, kind: 'test' })]
175
+ }
176
+ if (role === 'implementer' && !fix.code_answered) {
177
+ return [reviewFixSection({ fix, kind: 'code' })]
178
+ }
179
+ if (role === handoffRole({ fix })) {
180
+ return [
181
+ handoffSection({
182
+ fix,
183
+ role,
184
+ heading: `Review fix round ${fix.round}`,
185
+ }),
186
+ ]
187
+ }
188
+ return []
189
+ }
190
+
191
+ /**
192
+ * A fresh agent session, handed the run's newest notes. With `follow_up`,
193
+ * it stands in for a follow-up whose session is gone (a crash cut it off),
194
+ * and its prompt carries the follow-up message.
195
+ */
196
+ export const launch = ({
197
+ role,
198
+ snapshot,
199
+ ticket,
200
+ progress,
201
+ run_notes,
202
+ follow_up,
203
+ }: StepArgs & { role: AgentRole; follow_up?: string }): BuildAction => ({
204
+ type: 'launch_agent',
205
+ ticket: ticket.number,
206
+ role,
207
+ prompt: rolePrompt({
208
+ role,
209
+ spec: snapshot.spec,
210
+ ticket,
211
+ refactor: isRefactorTicket({ ticket }),
212
+ // A test update names the bad test in its own section.
213
+ bad_test: progress.test_update === null ? progress.bad_test : null,
214
+ bad_test_by: progress.bad_test_source ?? undefined,
215
+ sections: [
216
+ ...promptSections({ role, progress }),
217
+ ...(follow_up === undefined
218
+ ? []
219
+ : [followUpSection({ message: follow_up })]),
220
+ ],
221
+ run_notes,
222
+ }),
223
+ may_edit_tests: mayEditTests({ role, ticket }),
224
+ })
225
+
226
+ /**
227
+ * A fresh test-writer's or implementer's launch (a ticket's or the final
228
+ * review's fixers), with `section` added to its prompt, such as the repo's
229
+ * prepare command. Any other step as it is. With `only`, just that role's
230
+ * launches get it (the feature map is the test-writer's, #583).
231
+ */
232
+ export const withWriterSection = ({
233
+ step,
234
+ section,
235
+ only,
236
+ }: {
237
+ step: BuildAction
238
+ section: string
239
+ only?: 'test-writer' | 'implementer'
240
+ }): BuildAction => {
241
+ const writesCode =
242
+ (step.type === 'launch_agent' || step.type === 'launch_final_fixer') &&
243
+ (step.role === 'test-writer' || step.role === 'implementer') &&
244
+ (only === undefined || step.role === only)
245
+ return writesCode
246
+ ? { ...step, prompt: `${step.prompt}\n\n${section}` }
247
+ : step
248
+ }
249
+
250
+ /**
251
+ * In a repo with changesets, the writers are told not to write one (#507),
252
+ * and every reviewer (the ticket-reviewer and each lens) that the engine adds
253
+ * the run's one at PR time (#507, #545).
254
+ */
255
+ export const withChangesetsSections = (step: BuildAction): BuildAction =>
256
+ step.type === 'launch_lens' ||
257
+ (step.type === 'launch_agent' && step.role === 'ticket-reviewer')
258
+ ? { ...step, prompt: `${step.prompt}\n\n${CHANGESETS_LENS_SECTION}` }
259
+ : withWriterSection({ step, section: CHANGESETS_SECTION })
@@ -0,0 +1,282 @@
1
+ /**
2
+ * The engine's commits: the leftover scan, then a commit of a worktree's
3
+ * changes (`commitIn`), with agents' changesets dropped first and a crashed
4
+ * try's commit adopted. Serves the `commit_ticket` step and the final review.
5
+ */
6
+ import { rm } from 'node:fs/promises'
7
+ import { basename, join } from 'node:path'
8
+
9
+ import { need, ticketWorktree, type BuildContext } from './build-context'
10
+ import type { BuildAction } from './decide-build'
11
+ import { leaveOutIn, ticketChanges } from './worktree-changes'
12
+
13
+ import {
14
+ importStem,
15
+ isAgentChangeset,
16
+ leftoverPasses,
17
+ nameCheckedFiles,
18
+ newCodeFiles,
19
+ scanLeftovers,
20
+ } from '../gates/leftover-scan'
21
+ import { testFilesAmong } from '../gates/test-runner'
22
+ import type { FileChange } from '../git/git-adapter'
23
+ import { pathText } from '../git/path-text'
24
+ import { isTestHelper } from '../guards/role-rules'
25
+ import type { CommitStage } from '../journal/journal-record'
26
+
27
+ /** Whether anything besides the file itself mentions a new code file. */
28
+ const isUsed = async ({
29
+ context,
30
+ cwd,
31
+ path,
32
+ added_texts,
33
+ }: {
34
+ context: BuildContext
35
+ cwd: string
36
+ path: string
37
+ added_texts: Record<string, string>
38
+ }): Promise<boolean> => {
39
+ const stem = importStem(path)
40
+ const tracked = await context.git.filesMentioning({ cwd, text: stem })
41
+ if (tracked.some((file) => file !== path)) return true
42
+ return Object.entries(added_texts).some(
43
+ ([file, text]) => file !== path && text.includes(stem)
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Whether a file other than itself and the tests names this scratch-named
49
+ * file or new markdown (#520) by its basename, as a TOC, `package.json`, a
50
+ * doc index, or a config does: a
51
+ * tracked file, or one this change adds (a new addon's TOC names its new
52
+ * modules). A mention in a test doesn't make a file the repo's.
53
+ */
54
+ const isNamed = async ({
55
+ context,
56
+ cwd,
57
+ path,
58
+ added_texts,
59
+ }: {
60
+ context: BuildContext
61
+ cwd: string
62
+ path: string
63
+ added_texts: Record<string, string>
64
+ }): Promise<boolean> => {
65
+ const name = basename(path)
66
+ const tracked = await context.git.filesMentioning({ cwd, text: name })
67
+ const added = Object.entries(added_texts)
68
+ .filter(([, text]) => text.includes(name))
69
+ .map(([file]) => file)
70
+ const files = [...tracked, ...added].filter((file) => file !== path)
71
+ const tests = testFilesAmong({
72
+ files,
73
+ test_file_patterns: context.config.test_file_patterns,
74
+ })
75
+ return files.some((file) => !tests.includes(file))
76
+ }
77
+
78
+ /**
79
+ * In a repo with changesets, removes the changesets an agent wrote (see
80
+ * `isAgentChangeset`) from the worktree and journals `changeset_dropped`:
81
+ * the engine writes the run's one itself. Returns the other changes.
82
+ */
83
+ export const dropAgentChangesets = async ({
84
+ context,
85
+ cwd,
86
+ ticket,
87
+ changes,
88
+ }: {
89
+ context: BuildContext
90
+ cwd: string
91
+ ticket: number | null
92
+ changes: FileChange[]
93
+ }): Promise<FileChange[]> => {
94
+ if (!context.state.changesets) return changes
95
+ const paths = changes.filter(isAgentChangeset).map(({ path }) => path)
96
+ if (paths.length === 0) return changes
97
+ for (const path of paths) await rm(join(cwd, path), { force: true })
98
+ context.journal.append({
99
+ kind: 'changeset_dropped',
100
+ ticket,
101
+ role: null,
102
+ content: { paths },
103
+ })
104
+ return changes.filter(({ path }) => !paths.includes(path))
105
+ }
106
+
107
+ /**
108
+ * The commit a crash left behind at `cwd`: HEAD carries `message` and no
109
+ * record names it yet, so the step's first try made it and died before
110
+ * journaling it. `null` when HEAD is no such commit.
111
+ */
112
+ const leftCommit = async ({
113
+ context,
114
+ cwd,
115
+ message,
116
+ }: {
117
+ context: BuildContext
118
+ cwd: string
119
+ message: string
120
+ }): Promise<{ sha: string; message: string; files: string[] } | null> => {
121
+ const last = await context.git.lastCommit({ cwd })
122
+ if (last.message !== message.trim()) return null
123
+ const journaled = context.journal
124
+ .read()
125
+ .some(
126
+ (record) =>
127
+ (record.kind === 'commit_made' &&
128
+ record.content.sha === last.sha) ||
129
+ (record.kind === 'ticket_joined' &&
130
+ record.content.ok &&
131
+ record.content.shas.includes(last.sha))
132
+ )
133
+ return journaled ? null : { sha: last.sha, message, files: last.files }
134
+ }
135
+
136
+ /**
137
+ * The leftover scan, then an engine commit of everything in `cwd`, both
138
+ * journaled. What the prepare command made (`prepare_made`) and untracked
139
+ * links pointing outside the checkout (see `leaveOutIn`) are neither
140
+ * scanned nor committed. An agent's changeset is dropped before the scan
141
+ * (see `dropAgentChangesets`). A hit blocks the commit. A redo after a crash that
142
+ * already committed (nothing left to commit, and HEAD is an unjournaled
143
+ * commit with this message) journals that commit instead of making
144
+ * another; its scan, of a clean worktree, finds nothing, as the first try's
145
+ * did before it committed.
146
+ *
147
+ * Nothing to commit never runs `git commit`, which would fail (#494): the
148
+ * current commit is journaled with no files instead. That is a `fix` round
149
+ * whose every finding was a "won't fix", a green stage whose change a
150
+ * rebase found already on the run branch, or one whose every change came
151
+ * from prepare. (A red stage with nothing to commit fails its red check
152
+ * first, so it gets here only if the worktree changed after that check.)
153
+ */
154
+ export const commitIn = async ({
155
+ context,
156
+ cwd,
157
+ ticket,
158
+ stage,
159
+ message,
160
+ mention_text,
161
+ }: {
162
+ context: BuildContext
163
+ cwd: string
164
+ /** `null` for the final review. */
165
+ ticket: number | null
166
+ stage: CommitStage
167
+ message: string
168
+ /** The spec and ticket text a new markdown file may be named in. */
169
+ mention_text: string
170
+ }) => {
171
+ const leave_out = await leaveOutIn({ context, cwd, ticket })
172
+ const changes = await dropAgentChangesets({
173
+ context,
174
+ cwd,
175
+ ticket,
176
+ changes: await ticketChanges({ context, cwd, leave_out }),
177
+ })
178
+ const test_files = testFilesAmong({
179
+ files: changes.map(({ path }) => path),
180
+ test_file_patterns: context.config.test_file_patterns,
181
+ })
182
+ const added = changes.filter(({ change }) => change === 'added')
183
+ const added_texts: Record<string, string> = {}
184
+ for (const { path } of added) {
185
+ added_texts[path] = (await pathText({ path: join(cwd, path) })) ?? ''
186
+ }
187
+ const used_code: Record<string, boolean> = {}
188
+ for (const path of newCodeFiles({ changes, test_files })) {
189
+ used_code[path] = await isUsed({ context, cwd, path, added_texts })
190
+ }
191
+ const named: string[] = []
192
+ for (const path of nameCheckedFiles({ changes })) {
193
+ // A test helper (#619) is the tests', so a test that uses it names it.
194
+ const helper = isTestHelper({ path, config: context.config })
195
+ if (
196
+ helper
197
+ ? await isUsed({ context, cwd, path, added_texts })
198
+ : await isNamed({ context, cwd, path, added_texts })
199
+ ) {
200
+ named.push(path)
201
+ }
202
+ }
203
+ const inputs = {
204
+ changes,
205
+ test_files,
206
+ mention_text,
207
+ used_code,
208
+ named,
209
+ leftover_allow: context.config.leftover_allow ?? [],
210
+ }
211
+ const hits = scanLeftovers(inputs)
212
+ const passed = leftoverPasses(inputs)
213
+ context.journal.append({
214
+ kind: 'leftover_scan',
215
+ ticket,
216
+ role: null,
217
+ content: { stage, hits, ...(passed.length > 0 ? { passed } : {}) },
218
+ })
219
+ if (hits.length > 0) return
220
+ const adopted =
221
+ changes.length === 0 && context.step.redo
222
+ ? await leftCommit({ context, cwd, message })
223
+ : null
224
+ if (adopted !== null) {
225
+ context.journal.append({
226
+ kind: 'commit_made',
227
+ ticket,
228
+ role: null,
229
+ content: { stage, ...adopted },
230
+ })
231
+ return
232
+ }
233
+ if (changes.length === 0) {
234
+ // Nothing to commit: `git commit` would fail. For a fix round, the
235
+ // re-review's new changes are then empty.
236
+ context.journal.append({
237
+ kind: 'commit_made',
238
+ ticket,
239
+ role: null,
240
+ content: {
241
+ stage,
242
+ sha: await context.git.head({ cwd }),
243
+ message,
244
+ files: [],
245
+ },
246
+ })
247
+ return
248
+ }
249
+ const commit = await context.git.commitAll({ cwd, message, leave_out })
250
+ context.journal.append({
251
+ kind: 'commit_made',
252
+ ticket,
253
+ role: null,
254
+ content: { stage, sha: commit.sha, message, files: commit.files },
255
+ })
256
+ }
257
+
258
+ export const commitTicket = async ({
259
+ context,
260
+ action,
261
+ }: {
262
+ context: BuildContext
263
+ action: Extract<BuildAction, { type: 'commit_ticket' }>
264
+ }) => {
265
+ const { path: cwd } = ticketWorktree({
266
+ state: context.state,
267
+ ticket: action.ticket,
268
+ })
269
+ const snapshot = need({
270
+ value: context.state.snapshot,
271
+ what: 'spec snapshot',
272
+ })
273
+ const ticket = snapshot.tickets[action.ticket]
274
+ await commitIn({
275
+ context,
276
+ cwd,
277
+ ticket: action.ticket,
278
+ stage: action.stage,
279
+ message: action.message,
280
+ mention_text: [snapshot.spec.body, ticket?.body ?? ''].join('\n'),
281
+ })
282
+ }