@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,71 +1,24 @@
1
- import type { AgentRole, BadTest, Finding } from './role-results'
1
+ import { runNotesSection, type PromptRunNote } from './prompt-sections'
2
+ import type { AgentRole, BadTest, BadTestSource } from './role-results'
2
3
 
3
4
  import type { SpecSnapshot, TicketSnapshot } from '../intake/intake-schemas'
4
- import type { RejoinCause } from '../journal/journal-record'
5
5
  import { agentAddress, canMessage } from '../messages/agent-messages'
6
6
 
7
- /**
8
- * Why and how a ticket was sent back onto the run branch, for the prompts of
9
- * the agents that fix it there: the cause, the run branch commit it now
10
- * starts from, the files that clashed (tests and code), and the findings of
11
- * the review that approved it.
12
- */
13
- export type RejoinContext = {
14
- cause: RejoinCause
15
- base_sha: string
16
- tests: string[]
17
- code: string[]
18
- earlier_findings: Finding[]
19
- }
20
-
21
- const fileList = (files: string[]): string =>
22
- files.map((file) => `- ${file}`).join('\n')
23
-
24
- /** One line on what happened to the ticket, for every rejoin section. */
25
- export const rejoinOpening = ({ rejoin }: { rejoin: RejoinContext }): string =>
26
- rejoin.cause === 'clash'
27
- ? `Other tickets joined the run branch after this ticket started, and this ticket's change clashed with them. ` +
28
- `The engine put this ticket's whole change back on top of the run branch (commit ${rejoin.base_sha}), as uncommitted changes in the worktree.`
29
- : `The gates failed on the run branch after this ticket joined it. ` +
30
- `The engine undid the join and put this ticket's whole change back on top of the run branch (commit ${rejoin.base_sha}), as uncommitted changes in the worktree.`
31
-
32
- /**
33
- * The section a fresh test-writer or implementer gets while it fixes a
34
- * ticket on top of the run branch: the clashed test files, or the clashed
35
- * code (or the failed gates). `null` for a role with nothing to fix there.
36
- * The reviewer's re-review is the review's own (`reviewSections`).
37
- *
38
- * @example
39
- * const section = rejoinSection({ role: 'test-writer', rejoin })
40
- */
41
- export const rejoinSection = ({
42
- role,
43
- rejoin,
44
- }: {
45
- role: AgentRole
46
- rejoin: RejoinContext
47
- }): string | null => {
48
- if (role === 'test-writer') {
49
- if (rejoin.tests.length === 0) return null
50
- return [
51
- '## This ticket clashed with the run branch',
52
- rejoinOpening({ rejoin }),
53
- `These test files have conflict markers:\n\n${fileList(rejoin.tests)}`,
54
- "Resolve the markers so the tests check both what the run branch already has and this ticket's criteria. " +
55
- 'Change nothing else. Answer with the full criterion mapping again.',
56
- ].join('\n\n')
57
- }
58
- if (role === 'implementer') {
59
- return [
60
- '## This ticket is being fixed on top of the run branch',
61
- rejoinOpening({ rejoin }),
62
- rejoin.code.length > 0
63
- ? `These files have conflict markers:\n\n${fileList(rejoin.code)}\n\nResolve them so the code keeps both what the run branch has and what this ticket adds, and make every gate pass.`
64
- : 'Make every gate pass on top of the run branch.',
65
- ].join('\n\n')
66
- }
67
- return null
68
- }
7
+ export {
8
+ rejoinOpening,
9
+ rejoinSection,
10
+ testUpdateSection,
11
+ type JoinedSince,
12
+ type RejoinContext,
13
+ } from './rejoin-prompts'
14
+ export {
15
+ CHANGESETS_LENS_SECTION,
16
+ CHANGESETS_SECTION,
17
+ featureMapSection,
18
+ prepareSection,
19
+ runNotesSection,
20
+ type PromptRunNote,
21
+ } from './prompt-sections'
69
22
 
70
23
  /** A refactor ticket's implementer's task, in place of the usual one. */
71
24
  const REFACTOR_TASK =
@@ -73,11 +26,21 @@ const REFACTOR_TASK =
73
26
  'Make every gate pass. You may follow renames and moves into test files, but you must not change what a test checks. ' +
74
27
  'If a test is wrong, answer "bad_test" with your reason.'
75
28
 
76
- /** What a fresh test-writer is told when it replaces a bad test. */
77
- const badTestSection = ({ bad_test }: { bad_test: BadTest }): string =>
29
+ /**
30
+ * What a fresh test-writer is told when it replaces a bad test: the
31
+ * implementer sent it back, or the engine found it passes with no
32
+ * `expect()`, so it checks nothing (#581).
33
+ */
34
+ const badTestSection = ({
35
+ bad_test,
36
+ by,
37
+ }: {
38
+ bad_test: BadTest
39
+ by: BadTestSource
40
+ }): string =>
78
41
  [
79
42
  '## A test was sent back as a bad test',
80
- `The implementer says this test is wrong:\n\n- File: ${bad_test.file}\n- Test: ${bad_test.name}\n- Reason: ${bad_test.reason}`,
43
+ `${by === 'engine' ? 'The engine found this test checks nothing' : 'The implementer says this test is wrong'}:\n\n- File: ${bad_test.file}\n- Test: ${bad_test.name}\n- Reason: ${bad_test.reason}`,
81
44
  'Fix or replace it so it checks what its criterion asks. Keep the other tests unless they are wrong too. ' +
82
45
  'Answer with the full criterion mapping again: every criterion, the kept tests included.',
83
46
  ].join('\n\n')
@@ -88,52 +51,14 @@ const REFACTOR_REVIEW_TASK =
88
51
  "No test's meaning changed, no test was weakened or dropped, and no new behavior was added. " +
89
52
  'A behavior change is a blocker.'
90
53
 
91
- /** A run note as a later agent's prompt lists it. */
92
- export type PromptRunNote = { ticket: number; role: AgentRole; note: string }
93
-
94
- /** The notes earlier agents left, oldest first; nothing when there are none. */
95
- export const runNotesSection = ({
96
- run_notes,
97
- }: {
98
- run_notes: PromptRunNote[]
99
- }): string[] =>
100
- run_notes.length === 0
101
- ? []
102
- : [
103
- '## Run notes from earlier agents in this run',
104
- run_notes
105
- .map(
106
- ({ ticket, role, note }) =>
107
- `- ${note} (${role}, #${ticket})`
108
- )
109
- .join('\n'),
110
- ]
111
-
112
- /**
113
- * The section a test-writer or implementer gets when the repo has a prepare
114
- * command: the engine runs it before its test runs, so tests that need its
115
- * outputs may fail when the agent runs them itself.
116
- *
117
- * @example
118
- * prepareSection({ command: 'bun run build:rom' })
119
- * // '## This repo has a prepare command\n\n...'
120
- */
121
- export const prepareSection = ({ command }: { command: string }): string =>
122
- [
123
- '## This repo has a prepare command',
124
- `The engine runs \`${command}\` in the worktree before each of its test runs (the red check and the gates), so tests that need its outputs find them up to date. ` +
125
- "Never run it yourself. Tests that need its outputs may fail when you run them, and still pass at the engine's checks. " +
126
- "If the engine's prepare command fails after your turn, its output comes back to you like a failed check.",
127
- ].join('\n\n')
128
-
129
54
  /** What each lens judges the whole run branch on, in one paragraph. */
130
55
  const LENS_TASK_TEXT =
131
56
  'You are one lens of the final review: a fresh, read-only reviewer of the WHOLE run branch, every ticket of the spec together. Write nothing.'
132
57
 
133
58
  const ROLE_TASKS: Record<AgentRole, string> = {
134
59
  'test-writer':
135
- 'Write failing tests for every acceptance criterion below. Edit test files only. ' +
136
- 'Map each criterion id to the tests that check it.',
60
+ 'Write failing tests for every acceptance criterion below: the fewest that prove each one. Edit test files only. ' +
61
+ 'Map every test you add to the criterion it checks.',
137
62
  implementer:
138
63
  'Make every gate pass. Never edit a test file. ' +
139
64
  'If a test is wrong, answer "bad_test" with your reason. ' +
@@ -172,7 +97,10 @@ export const roleTask = ({ role }: { role: AgentRole }): string =>
172
97
  * @param refactor - The ticket is a refactor ticket: the implementer gets
173
98
  * the refactor task (no new behavior; it may follow renames into tests).
174
99
  * @param bad_test - For a fresh test-writer after a bad-test bounce: the
175
- * test the implementer sent back, and why.
100
+ * test the implementer sent back, or the engine found checks nothing
101
+ * (#581), and why.
102
+ * @param bad_test_by - Who sent `bad_test` back: the implementer (the
103
+ * default) or the engine.
176
104
  * @param sections - More sections for the end, such as a reviewer's diff
177
105
  * and gate results, or a review fixer's findings.
178
106
  * @param run_notes - Notes earlier agents in the run left, oldest first,
@@ -187,6 +115,7 @@ export const rolePrompt = ({
187
115
  ticket,
188
116
  refactor,
189
117
  bad_test,
118
+ bad_test_by,
190
119
  sections,
191
120
  run_notes,
192
121
  }: {
@@ -195,6 +124,7 @@ export const rolePrompt = ({
195
124
  ticket: TicketSnapshot
196
125
  refactor?: boolean
197
126
  bad_test?: BadTest | null
127
+ bad_test_by?: BadTestSource
198
128
  sections?: string[]
199
129
  run_notes?: PromptRunNote[]
200
130
  }): string =>
@@ -216,7 +146,7 @@ export const rolePrompt = ({
216
146
  '## Acceptance criteria',
217
147
  ticket.criteria.map(({ id, text }) => `- ${id}: ${text}`).join('\n'),
218
148
  ...(role === 'test-writer' && bad_test
219
- ? [badTestSection({ bad_test })]
149
+ ? [badTestSection({ bad_test, by: bad_test_by ?? 'implementer' })]
220
150
  : []),
221
151
  ...(sections ?? []),
222
152
  // Recalled memories (#370) are added after these by the decision step.
@@ -82,12 +82,14 @@ export const CriterionTestsSchema = z.object({
82
82
  export type CriterionTests = z.infer<typeof CriterionTestsSchema>
83
83
 
84
84
  /**
85
- * A review fixer's answer to one finding it was sent: `fixed`, or
86
- * `wont_fix` with its reason (pushback the next fresh reviewer rules on).
85
+ * A review fixer's answer to one finding it was sent: `fixed`, `wont_fix`
86
+ * with its reason (pushback the next fresh reviewer rules on), or
87
+ * `other_role` with its reason: the other writer role must fix it, or must
88
+ * fix it too (#616).
87
89
  */
88
90
  export const FindingResponseSchema = z.object({
89
91
  finding_id: z.string().min(1),
90
- response: z.enum(['fixed', 'wont_fix']),
92
+ response: z.enum(['fixed', 'wont_fix', 'other_role']),
91
93
  reason: z.string().default(''),
92
94
  })
93
95
 
@@ -118,6 +120,43 @@ const SETUP_FIELDS = {
118
120
  setup_change: SetupChangeSchema.nullable().default(null),
119
121
  }
120
122
 
123
+ /**
124
+ * Why a writer says a failed red check or failed gates are not its
125
+ * ticket's fault (#613): a port already in use, an old test that fails at
126
+ * baseline too. The engine checks the claim by rerunning the check.
127
+ */
128
+ export const EnvironmentSchema = z.object({
129
+ evidence: z.string().trim().min(1),
130
+ })
131
+
132
+ export type Environment = z.infer<typeof EnvironmentSchema>
133
+
134
+ /**
135
+ * What a writer that blames the environment adds to its result, with the
136
+ * outcome `environment`.
137
+ */
138
+ const ENVIRONMENT_FIELDS = {
139
+ environment: EnvironmentSchema.nullable().default(null),
140
+ }
141
+
142
+ /** A file mode a writer asks the engine to set (#618). */
143
+ export const FileModeSchema = z.object({
144
+ /** Worktree-relative path of the file. */
145
+ path: z.string().min(1),
146
+ executable: z.boolean(),
147
+ })
148
+
149
+ export type FileMode = z.infer<typeof FileModeSchema>
150
+
151
+ /**
152
+ * The owner chores a writer asks the engine to do after its turn (#618):
153
+ * file modes to set, and the repo's generators to run, by name.
154
+ */
155
+ const CHORE_FIELDS = {
156
+ file_modes: z.array(FileModeSchema).default([]),
157
+ run_generators: z.array(z.string().min(1)).default([]),
158
+ }
159
+
121
160
  const SHARED_FIELDS = {
122
161
  summary: z.string().default(''),
123
162
  /** Calls the agent made by itself when something was unclear. */
@@ -143,7 +182,8 @@ export type DoneCommit = z.infer<typeof DoneCommitSchema>
143
182
  * it and `criteria` the existing tests that cover each criterion; the engine
144
183
  * counts the ticket as done. `needs_setup_change` says the tests can't be
145
184
  * written without a change to a test setup file (`setup_change`), which
146
- * only the user may make.
185
+ * only the user may make. `environment` says a failed red check is not the
186
+ * ticket's fault (#613), with its evidence; the engine reruns the check.
147
187
  */
148
188
  export const TestWriterResultSchema = z.object({
149
189
  outcome: z.enum([
@@ -151,6 +191,7 @@ export const TestWriterResultSchema = z.object({
151
191
  'nothing_new_to_test',
152
192
  'already_done',
153
193
  'needs_setup_change',
194
+ 'environment',
154
195
  ]),
155
196
  /**
156
197
  * For each criterion id (AC1, AC2, ...), the tests that check it: new
@@ -160,6 +201,8 @@ export const TestWriterResultSchema = z.object({
160
201
  /** With `already_done`: the base branch's commits that did the work. */
161
202
  done_by: z.array(DoneCommitSchema).default([]),
162
203
  ...SETUP_FIELDS,
204
+ ...ENVIRONMENT_FIELDS,
205
+ ...CHORE_FIELDS,
163
206
  ...SHARED_FIELDS,
164
207
  ...FIXER_FIELDS,
165
208
  })
@@ -175,15 +218,25 @@ export const BadTestSchema = z.object({
175
218
 
176
219
  export type BadTest = z.infer<typeof BadTestSchema>
177
220
 
221
+ /**
222
+ * Who sent a test back as bad: the implementer, who says it is wrong, or the
223
+ * engine, which found a mapped test that passes with no `expect()` (#581).
224
+ */
225
+ export type BadTestSource = 'implementer' | 'engine'
226
+
178
227
  /**
179
228
  * The implementer's result. `bad_test` sends a wrong test back;
180
229
  * `needs_setup_change` says the gates can't pass without a change to a test
181
- * setup file (`setup_change`), which only the user may make.
230
+ * setup file (`setup_change`), which only the user may make;
231
+ * `environment` says failed gates are not the ticket's fault (#613), with
232
+ * its evidence, and the engine reruns them.
182
233
  */
183
234
  export const ImplementerResultSchema = z.object({
184
- outcome: z.enum(['done', 'bad_test', 'needs_setup_change']),
235
+ outcome: z.enum(['done', 'bad_test', 'needs_setup_change', 'environment']),
185
236
  bad_test: BadTestSchema.nullable().default(null),
186
237
  ...SETUP_FIELDS,
238
+ ...ENVIRONMENT_FIELDS,
239
+ ...CHORE_FIELDS,
187
240
  ...SHARED_FIELDS,
188
241
  ...FIXER_FIELDS,
189
242
  })
@@ -251,6 +304,37 @@ export const LensReviewResultSchema = TicketReviewResultSchema
251
304
 
252
305
  export type LensReviewResult = z.infer<typeof LensReviewResultSchema>
253
306
 
307
+ /**
308
+ * How dangerous the branch is to merge (#513), as the integration lens
309
+ * judges it. A **one-way door** is a change a revert can't undo out in the
310
+ * world: a migration that drops data, a publish, sent messages, a deploy.
311
+ * A two-way door a revert undoes. The **blast radius** is how much breaks
312
+ * if the change is wrong. Each comes with a one-line reason.
313
+ */
314
+ export const MergeDangerSchema = z.object({
315
+ door: z.enum(['one_way', 'two_way']),
316
+ door_reason: z.string(),
317
+ blast_radius: z.enum(['small', 'medium', 'large']),
318
+ blast_radius_reason: z.string(),
319
+ })
320
+
321
+ export type MergeDanger = z.infer<typeof MergeDangerSchema>
322
+
323
+ /**
324
+ * The integration lens's result (#513): a lens result, plus, on its first
325
+ * round, the PR's Summary picture and Merge Danger. `summary_picture` is the
326
+ * smallest visual that makes the change clear (a call tree, a file tree, a
327
+ * Mermaid diagram, or a diff sketch), as markdown with its fenced block. A
328
+ * re-review gives null for both. Both default to null, so older journals
329
+ * still parse.
330
+ */
331
+ export const IntegrationLensResultSchema = LensReviewResultSchema.extend({
332
+ summary_picture: z.string().nullable().default(null),
333
+ merge_danger: MergeDangerSchema.nullable().default(null),
334
+ })
335
+
336
+ export type IntegrationLensResult = z.infer<typeof IntegrationLensResultSchema>
337
+
254
338
  /** The most memories one learner may propose. */
255
339
  export const MAX_PROPOSED_MEMORIES = 10
256
340
 
@@ -306,7 +390,10 @@ export const RoleResultSchema = z.discriminatedUnion('role', [
306
390
  lensResult('architecture-lens'),
307
391
  lensResult('simplification-lens'),
308
392
  lensResult('security-lens'),
309
- lensResult('integration-lens'),
393
+ z.object({
394
+ role: z.literal('integration-lens'),
395
+ result: IntegrationLensResultSchema,
396
+ }),
310
397
  lensResult('rules-lens'),
311
398
  z.object({ role: z.literal('learner'), result: LearnerResultSchema }),
312
399
  ])
@@ -336,6 +423,18 @@ export const parseRoleResult = ({
336
423
  }
337
424
  }
338
425
  const { result } = parsed.data
426
+ // "Environment" is only an answer with its evidence (#613).
427
+ if (
428
+ (parsed.data.role === 'test-writer' ||
429
+ parsed.data.role === 'implementer') &&
430
+ parsed.data.result.outcome === 'environment' &&
431
+ parsed.data.result.environment === null
432
+ ) {
433
+ return {
434
+ ok: false,
435
+ error: `The ${role}'s "environment" gives no evidence: say in "environment.evidence" why the failure is not this ticket's fault.`,
436
+ }
437
+ }
339
438
  // "Already done" is only an answer with its evidence (#484).
340
439
  if (parsed.data.role === 'test-writer') {
341
440
  const { outcome, done_by, criteria } = parsed.data.result
@@ -50,6 +50,8 @@ export type ScriptedTurn = {
50
50
  result?: unknown
51
51
  failure?: LauncherFailure
52
52
  error?: string
53
+ /** With `failure`: the turn reached its role's turn limit (#543). */
54
+ out_of_turns?: boolean
53
55
  /** A session summary for the engine to journal. */
54
56
  session?: AgentSession
55
57
  }
@@ -64,6 +66,11 @@ export type ScriptedCall = {
64
66
  session_id: string
65
67
  /** Only on launches. */
66
68
  may_edit_tests?: boolean
69
+ /**
70
+ * The model a launch asked for; a follow-up's is its session's, `null`
71
+ * for a session the launcher doesn't know.
72
+ */
73
+ model: string | null
67
74
  /** What each of the turn's tool calls handed over, in order. */
68
75
  delivered: string[]
69
76
  }
@@ -100,8 +107,11 @@ export const createScriptedLauncher = ({
100
107
  }): ScriptedLauncher => {
101
108
  const remaining = [...turns]
102
109
  const calls: ScriptedCall[] = []
103
- /** Each open session's messaging, kept for its follow-ups. */
104
- const sessions = new Map<string, AgentMessaging | null>()
110
+ /** Each open session's messaging and model, kept for its follow-ups. */
111
+ const sessions = new Map<
112
+ string,
113
+ { messaging: AgentMessaging | null; model: string }
114
+ >()
105
115
 
106
116
  const toolsFor = ({
107
117
  messaging,
@@ -161,6 +171,7 @@ export const createScriptedLauncher = ({
161
171
  failure: turn.failure,
162
172
  session_id,
163
173
  error: turn.error ?? `The scripted ${role} failed.`,
174
+ ...(turn.out_of_turns === true ? { out_of_turns: true } : {}),
164
175
  ...session,
165
176
  }
166
177
  }
@@ -180,9 +191,10 @@ export const createScriptedLauncher = ({
180
191
  cwd,
181
192
  may_edit_tests,
182
193
  messaging,
194
+ model,
183
195
  }) => {
184
196
  const session_id = `scripted-${role}-${ticket}-${calls.length + 1}`
185
- sessions.set(session_id, messaging)
197
+ sessions.set(session_id, { messaging, model })
186
198
  const call: ScriptedCall = {
187
199
  kind: 'launch',
188
200
  role,
@@ -190,6 +202,7 @@ export const createScriptedLauncher = ({
190
202
  prompt,
191
203
  session_id,
192
204
  may_edit_tests,
205
+ model,
193
206
  delivered: [],
194
207
  }
195
208
  calls.push(call)
@@ -197,25 +210,27 @@ export const createScriptedLauncher = ({
197
210
  return play({ role, ticket, cwd, session_id, tools })
198
211
  },
199
212
  followUp: async ({ session_id, role, ticket, message, cwd }) => {
213
+ const open = sessions.get(session_id)
200
214
  const call: ScriptedCall = {
201
215
  kind: 'follow_up',
202
216
  role,
203
217
  ticket,
204
218
  prompt: message,
205
219
  session_id,
220
+ model: open?.model ?? null,
206
221
  delivered: [],
207
222
  }
208
223
  calls.push(call)
209
- const messaging = sessions.get(session_id)
210
- if (messaging === undefined) {
224
+ if (open === undefined) {
211
225
  return {
212
226
  ok: false,
213
227
  failure: 'engine',
214
228
  session_id,
229
+ session_gone: true,
215
230
  error: `No scripted session ${session_id}.`,
216
231
  }
217
232
  }
218
- const tools = toolsFor({ messaging, call })
233
+ const tools = toolsFor({ messaging: open.messaging, call })
219
234
  return play({ role, ticket, cwd, session_id, tools })
220
235
  },
221
236
  closeSession: async ({ session_id }) => {
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The agent sessions the Claude launcher keeps open between turns: the pool
3
+ * of them by session id, a turn on one (`runTurn`), and closing one or all.
4
+ * A success or an agent failure keeps a session open for follow-ups until it
5
+ * sits idle too long; a stop, an engine failure, or a timeout closes it.
6
+ */
7
+ import { AgentSessionSchema, type AgentTurn } from './agent-launcher'
8
+ import type { Outcome } from './claude-messages'
9
+ import { nextOutcome, type OpenSession } from './open-session'
10
+
11
+ /**
12
+ * Makes the pool of open sessions. A turn gives up after `turn_timeout_ms`;
13
+ * a session left open closes after `idle_timeout_ms` of sitting idle.
14
+ *
15
+ * @example
16
+ * const pool = createSessionPool({ turn_timeout_ms: 40 * 60_000, idle_timeout_ms: 30 * 60_000 })
17
+ * const turn = await pool.runTurn({ open, message, started: Date.now(), fallback_id })
18
+ * await pool.closeAll()
19
+ */
20
+ export const createSessionPool = ({
21
+ turn_timeout_ms: timeoutMs,
22
+ idle_timeout_ms: idleMs,
23
+ }: {
24
+ turn_timeout_ms: number
25
+ idle_timeout_ms: number
26
+ }) => {
27
+ const sessions = new Map<string, OpenSession>()
28
+
29
+ const close = async (open: OpenSession): Promise<void> => {
30
+ clearTimeout(open.idle_timer)
31
+ for (const [id, each] of sessions) {
32
+ if (each === open) sessions.delete(id)
33
+ }
34
+ open.input.close()
35
+ open.running.close()
36
+ await Promise.race([open.pump, Bun.sleep(5_000)])
37
+ }
38
+
39
+ /** Keeps a session open for follow-ups until it sits idle too long. */
40
+ const keepOpen = (open: OpenSession, session_id: string): void => {
41
+ sessions.set(session_id, open)
42
+ clearTimeout(open.idle_timer)
43
+ open.idle_timer = setTimeout(() => void close(open), idleMs)
44
+ open.idle_timer.unref?.()
45
+ }
46
+
47
+ /** Resets the summary's per-turn fields; the session's own stay. */
48
+ const startTurn = (open: OpenSession): void => {
49
+ clearTimeout(open.idle_timer)
50
+ const fresh = AgentSessionSchema.parse({})
51
+ open.summary = {
52
+ ...fresh,
53
+ session_id: open.summary.session_id,
54
+ model: open.summary.model,
55
+ claude_code_version: open.summary.claude_code_version,
56
+ api_key_source: open.summary.api_key_source,
57
+ subscription_type: open.summary.subscription_type,
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Sends one message and waits for the turn's outcome. A success or an
63
+ * agent failure keeps the session open for follow-ups; a stop, an
64
+ * engine failure, or a timeout closes it.
65
+ */
66
+ const runTurn = async ({
67
+ open,
68
+ message,
69
+ started,
70
+ fallback_id,
71
+ }: {
72
+ open: OpenSession
73
+ message: string
74
+ started: number
75
+ /** The id to report if the session never named itself. */
76
+ fallback_id: string
77
+ }): Promise<AgentTurn> => {
78
+ const waiting = nextOutcome(open)
79
+ open.input.push({
80
+ type: 'user',
81
+ message: { role: 'user', content: message },
82
+ parent_tool_use_id: null,
83
+ })
84
+ let timer: ReturnType<typeof setTimeout> | undefined
85
+ const timedOut = new Promise<'timeout'>((resolve) => {
86
+ timer = setTimeout(() => resolve('timeout'), timeoutMs)
87
+ })
88
+ const raced = await Promise.race([waiting, timedOut])
89
+ clearTimeout(timer)
90
+ const outcome: Outcome =
91
+ raced === 'timeout'
92
+ ? {
93
+ ok: false,
94
+ failure: 'agent',
95
+ error: `No result within ${timeoutMs} ms; the agent was interrupted.`,
96
+ }
97
+ : raced
98
+ if (raced === 'timeout') {
99
+ await open.running.interrupt().catch(() => undefined)
100
+ }
101
+ const session_id = open.summary.session_id ?? fallback_id
102
+ const session = {
103
+ ...open.summary,
104
+ duration_ms: Date.now() - started,
105
+ }
106
+ const keep =
107
+ raced !== 'timeout' &&
108
+ open.ended === null &&
109
+ open.summary.session_id !== null
110
+ if (keep) keepOpen(open, session_id)
111
+ else await close(open)
112
+ return { ...outcome, session_id, session }
113
+ }
114
+
115
+ /** Closes every session still open. */
116
+ const closeAll = async (): Promise<void> => {
117
+ await Promise.all([...new Set(sessions.values())].map(close))
118
+ }
119
+
120
+ return { sessions, close, startTurn, runTurn, closeAll }
121
+ }
122
+
123
+ /** The pool `createSessionPool` makes. */
124
+ export type SessionPool = ReturnType<typeof createSessionPool>