@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,207 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * The reply buttons on stuck work (#503): which one-word replies the engine
5
+ * takes for each kind of stuck item, the words of the confirm step, the
6
+ * `/luca-unstick` command for "Help me", and what each button shows. Plain
7
+ * values only, so the panel, the chat rows, and the server share them.
8
+ */
9
+
10
+ /** The key of a ticket's "Needs you" item. */
11
+ export const ticketKey = ({ ticket }: { ticket: number }): string =>
12
+ `ticket-${ticket}`
13
+
14
+ /** The key of the final review's "Needs you" item. */
15
+ export const FINAL_KEY = 'final'
16
+
17
+ /** The key of the "Needs you" item for the whole run, stuck on its budget. */
18
+ export const RUN_KEY = 'run'
19
+
20
+ /**
21
+ * The replies the engine takes for one stuck item, as the owner would type
22
+ * them. A stuck ticket always gets its number (`retry #12`), since a bare
23
+ * word is only taken while one ticket is stuck. The final review takes a
24
+ * bare `retry`, `stop`, or `ship`; the run stuck on its budget a bare
25
+ * `retry` or `stop` (see the engine's README, "Replies"). An item this
26
+ * doesn't know gets none, so no button can post a word the engine refuses.
27
+ *
28
+ * @example
29
+ * allowedReplies({ key: 'ticket-12', ticket: 12 }) // ['retry #12', 'skip #12', 'stop']
30
+ * allowedReplies({ key: 'final', ticket: null }) // ['retry', 'stop', 'ship']
31
+ */
32
+ export const allowedReplies = ({
33
+ key,
34
+ ticket,
35
+ }: {
36
+ key: string
37
+ ticket: number | null
38
+ }): string[] => {
39
+ if (ticket !== null) {
40
+ return key === ticketKey({ ticket })
41
+ ? [`retry #${ticket}`, `skip #${ticket}`, 'stop']
42
+ : []
43
+ }
44
+ if (key === FINAL_KEY) return ['retry', 'stop', 'ship']
45
+ if (key === RUN_KEY) return ['retry', 'stop']
46
+ return []
47
+ }
48
+
49
+ /** A reply word and the ticket it names, as the engine journals it. */
50
+ export type ReplyParts = {
51
+ word: 'retry' | 'skip' | 'stop' | 'ship'
52
+ ticket: number | null
53
+ }
54
+
55
+ const REPLY_PATTERN = /^(retry|skip|stop|ship)(?: #(\d+))?$/
56
+
57
+ /** `retry` with an amount for the run stuck on its budget (#543). */
58
+ const AMOUNT_REPLY_PATTERN = /^retry \+\S+$/
59
+
60
+ /**
61
+ * Splits one of `allowedReplies`, or a `retry +<amount>` the owner typed,
62
+ * into its word and ticket, to match it with the engine's `reply_received`.
63
+ * `null` for anything else.
64
+ *
65
+ * @example
66
+ * replyParts({ reply: 'retry #12' }) // { word: 'retry', ticket: 12 }
67
+ * replyParts({ reply: 'retry +3M' }) // { word: 'retry', ticket: null }
68
+ */
69
+ export const replyParts = ({ reply }: { reply: string }): ReplyParts | null => {
70
+ if (AMOUNT_REPLY_PATTERN.test(reply)) return { word: 'retry', ticket: null }
71
+ const match = REPLY_PATTERN.exec(reply)
72
+ const word = z.enum(['retry', 'skip', 'stop', 'ship']).safeParse(match?.[1])
73
+ if (!match || !word.success) return null
74
+ return {
75
+ word: word.data,
76
+ ticket: match[2] === undefined ? null : Number(match[2]),
77
+ }
78
+ }
79
+
80
+ const ISSUE_URL_PATTERN =
81
+ /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/issues\/\d+/
82
+
83
+ /**
84
+ * The repo of a GitHub issue URL, as `owner/repo`; `null` for any other URL.
85
+ *
86
+ * @example
87
+ * repoOfIssueUrl({ url: 'https://github.com/o/r/issues/10' }) // 'o/r'
88
+ */
89
+ export const repoOfIssueUrl = ({ url }: { url: string }): string | null => {
90
+ const match = ISSUE_URL_PATTERN.exec(url)
91
+ return match ? `${match[1]}/${match[2]}` : null
92
+ }
93
+
94
+ /**
95
+ * The question before a reply is posted.
96
+ *
97
+ * @example
98
+ * confirmReplyText({ reply: 'retry #134', spec_number: 133 })
99
+ * // 'Post `retry #134` on spec #133?'
100
+ */
101
+ export const confirmReplyText = ({
102
+ reply,
103
+ spec_number,
104
+ }: {
105
+ reply: string
106
+ spec_number: number | null
107
+ }): string =>
108
+ `Post \`${reply}\` on ${spec_number === null ? 'the spec issue' : `spec #${spec_number}`}?`
109
+
110
+ /**
111
+ * The `/luca-unstick` command "Help me" puts in the chat: the run id, and
112
+ * the ticket for a stuck ticket (none for the final review or the budget).
113
+ *
114
+ * @example
115
+ * unstickCommand({ run_id: 'luca-20260928-101500-abcd', ticket: 134 })
116
+ * // '/luca-unstick luca-20260928-101500-abcd #134'
117
+ */
118
+ export const unstickCommand = ({
119
+ run_id,
120
+ ticket,
121
+ }: {
122
+ run_id: string
123
+ ticket: number | null
124
+ }): string =>
125
+ ticket === null
126
+ ? `/luca-unstick ${run_id}`
127
+ : `/luca-unstick ${run_id} #${ticket}`
128
+
129
+ /**
130
+ * API Response part: a reply the board posted on the spec issue, for one
131
+ * stuck item (its key and `since`), and whether the engine took it
132
+ * (`reply_received`). Kept in the plugin's memory only.
133
+ */
134
+ export const PostedReplySchema = z.object({
135
+ key: z.string(),
136
+ /** The stuck item's `since`: the same ticket stuck again is a new item. */
137
+ since: z.string(),
138
+ reply: z.string(),
139
+ posted_at: z.string(),
140
+ /** The comment's URL, as `gh issue comment` printed it. */
141
+ comment_url: z.string().nullable(),
142
+ /** Set once the engine journaled `reply_received` for it. */
143
+ taken_at: z.string().nullable(),
144
+ })
145
+
146
+ export type PostedReply = z.infer<typeof PostedReplySchema>
147
+
148
+ /** One reply button: its reply, and whether and why it can't be tapped. */
149
+ export type ReplyButton = {
150
+ reply: string
151
+ /** `ready`, `sending` (this one is in flight), `posted`, or `locked`. */
152
+ state: 'ready' | 'sending' | 'posted' | 'locked'
153
+ }
154
+
155
+ /**
156
+ * The buttons of one stuck item. Once a reply is posted (or while one is in
157
+ * flight) for this item, that button shows it and the others lock, so a
158
+ * second tap can't post again or post a second word the engine would
159
+ * refuse.
160
+ *
161
+ * @example
162
+ * replyButtons({ item, posted: [], sending: null })
163
+ * // [{ reply: 'retry #12', state: 'ready' }, ...]
164
+ */
165
+ export const replyButtons = ({
166
+ item,
167
+ posted,
168
+ sending,
169
+ }: {
170
+ item: { key: string; ticket: number | null; since: string }
171
+ posted: PostedReply[]
172
+ /** The reply being posted for this item now, if any. */
173
+ sending: string | null
174
+ }): ReplyButton[] => {
175
+ const done = postedFor({ item, posted })
176
+ return allowedReplies(item).map((reply) => {
177
+ if (done)
178
+ return { reply, state: done.reply === reply ? 'posted' : 'locked' }
179
+ if (sending !== null) {
180
+ return { reply, state: sending === reply ? 'sending' : 'locked' }
181
+ }
182
+ return { reply, state: 'ready' }
183
+ })
184
+ }
185
+
186
+ /** The reply posted for this stuck item, if any. */
187
+ export const postedFor = ({
188
+ item,
189
+ posted,
190
+ }: {
191
+ item: { key: string; since: string }
192
+ posted: PostedReply[]
193
+ }): PostedReply | null =>
194
+ posted.find(
195
+ (entry) => entry.key === item.key && entry.since === item.since
196
+ ) ?? null
197
+
198
+ /**
199
+ * What a posted reply says under the buttons.
200
+ *
201
+ * @example
202
+ * postedText({ posted }) // 'Posted `retry #12`. Waiting for the engine to read it.'
203
+ */
204
+ export const postedText = ({ posted }: { posted: PostedReply }): string =>
205
+ posted.taken_at === null
206
+ ? `Posted \`${posted.reply}\`. Waiting for the engine to read it (it checks about once a minute).`
207
+ : `Posted \`${posted.reply}\`. The engine took it.`
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import type { AgentRole } from './role-results'
4
+ import type { ProgressListener } from './turn-progress'
4
5
 
5
6
  import type { EngineConfig } from '../config/engine-config'
6
7
 
@@ -66,6 +67,11 @@ export const AgentSessionSchema = z.object({
66
67
  rate_limit_events: z.array(z.record(z.string(), z.unknown())).default([]),
67
68
  /** An assistant message came back with `error: "billing_error"`. */
68
69
  billing_error: z.boolean().default(false),
70
+ /**
71
+ * How many helpers (#627) the turn started. Their tokens are in
72
+ * `model_usage`. Older journals have none.
73
+ */
74
+ helpers_started: z.number().int().min(0).default(0),
69
75
  })
70
76
 
71
77
  export type AgentSession = z.infer<typeof AgentSessionSchema>
@@ -94,7 +100,10 @@ export type LauncherFailure = z.infer<typeof LauncherFailureSchema>
94
100
  * the same way; a success with no structured output is a failed try.
95
101
  * `session_id` names the session the turn ran in, so the engine can send that
96
102
  * same agent a follow-up in a fix loop. `session` is the launcher's summary
97
- * of the model session, for the journal.
103
+ * of the model session, for the journal. `session_gone` marks a follow-up
104
+ * that found its session closed or unknown (#521): no agent ran, and the
105
+ * engine hands the follow-up's message to a fresh agent. `out_of_turns`
106
+ * marks a turn that reached its role's turn limit (#543).
98
107
  */
99
108
  export type AgentTurn =
100
109
  | {
@@ -109,6 +118,8 @@ export type AgentTurn =
109
118
  error: string
110
119
  session_id?: string
111
120
  session?: AgentSession
121
+ session_gone?: boolean
122
+ out_of_turns?: boolean
112
123
  }
113
124
 
114
125
  /**
@@ -157,13 +168,23 @@ export type AgentLauncher = {
157
168
  * for its follow-ups.
158
169
  */
159
170
  messaging: AgentMessaging | null
171
+ /**
172
+ * The model's short name: `opus`, `sonnet` or `haiku`. The session's
173
+ * follow-ups run on it too.
174
+ */
175
+ model: string
176
+ /**
177
+ * Hears the turn's live progress (#628) each time it changes, before
178
+ * the turn ends, for the engine's live file. Left out, nothing hears it.
179
+ */
180
+ on_progress?: ProgressListener
160
181
  }) => Promise<AgentTurn>
161
182
  /**
162
183
  * Sends a follow-up message to an agent session that is still open, such
163
184
  * as a failed gate's output in a fix loop, and returns the session's new
164
- * result. The agent keeps everything it knew from its earlier turns, and
165
- * the guards it was launched with. A session the launcher doesn't know,
166
- * or one that has closed, is an engine failure.
185
+ * result. The agent keeps everything it knew from its earlier turns, the
186
+ * guards it was launched with, and its model. A session the launcher doesn't know,
187
+ * or one that has closed, is an engine failure with `session_gone`.
167
188
  *
168
189
  * @example
169
190
  * const turn = await launcher.followUp({ session_id, role: 'implementer', ticket: 11, message: 'lint failed: ...', cwd, config })
@@ -175,6 +196,8 @@ export type AgentLauncher = {
175
196
  message: string
176
197
  cwd: string
177
198
  config: EngineConfig
199
+ /** Hears the follow-up turn's live progress, as for `launch`. */
200
+ on_progress?: ProgressListener
178
201
  }) => Promise<AgentTurn>
179
202
  /**
180
203
  * Closes one agent session once nothing can send it a follow-up anymore,
@@ -0,0 +1,233 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { $ } from 'bun'
6
+
7
+ import type { Options, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
8
+ import { afterEach, beforeEach } from 'bun:test'
9
+
10
+ import type { AgentMessaging, AgentTurn } from './agent-launcher'
11
+ import { createClaudeLauncher, type AgentQuery } from './claude-launcher'
12
+ import { CLAUDE_MODEL } from './claude-options'
13
+ import type { AgentRole } from './role-results'
14
+
15
+ import type { EngineConfig } from '../config/engine-config'
16
+
17
+ /**
18
+ * Helpers shared by the launcher tests (`claude-launcher.test.ts` and the
19
+ * `claude-launcher-*.test.ts` files beside it).
20
+ *
21
+ * The launcher seam: `createClaudeLauncher` with a fake `query` that plays
22
+ * back SDK-shaped messages, and a fake `claude` path. No model, no process.
23
+ */
24
+
25
+ export const CONFIG: EngineConfig = {
26
+ checks: { test: 'bun test', types: 'bunx --bun tsc --noEmit' },
27
+ test_file_patterns: ['src/**/*.test.ts'],
28
+ test_setup_files: ['src/test-setup.ts'],
29
+ rule_files: [],
30
+ }
31
+
32
+ export const CLAUDE_PATH = '/opt/fake/bin/claude'
33
+
34
+ export const MAX_ACCOUNT = {
35
+ email: 'me@example.com',
36
+ subscriptionType: 'max',
37
+ apiKeySource: 'none',
38
+ apiProvider: 'firstParty',
39
+ }
40
+
41
+ export const INIT = {
42
+ type: 'system',
43
+ subtype: 'init',
44
+ apiKeySource: 'none',
45
+ model: 'claude-opus-5-5',
46
+ claude_code_version: '2.1.280',
47
+ session_id: 'session-1',
48
+ cwd: '/somewhere',
49
+ tools: ['Read', 'Grep', 'Glob', 'Bash', 'StructuredOutput'],
50
+ mcp_servers: [],
51
+ permissionMode: 'dontAsk',
52
+ }
53
+
54
+ export const APPROVE = {
55
+ verdict: 'approve',
56
+ findings: [],
57
+ summary: 'Fine.',
58
+ assumptions: [],
59
+ }
60
+
61
+ export const result = (fields: Record<string, unknown>) => ({
62
+ type: 'result',
63
+ subtype: 'success',
64
+ is_error: false,
65
+ num_turns: 3,
66
+ duration_ms: 1200,
67
+ total_cost_usd: 0.25,
68
+ usage: {
69
+ input_tokens: 10,
70
+ output_tokens: 20,
71
+ cache_read_input_tokens: 30,
72
+ cache_creation_input_tokens: 40,
73
+ },
74
+ permission_denials: [],
75
+ session_id: 'session-1',
76
+ ...fields,
77
+ })
78
+
79
+ type FakeCall = {
80
+ options: Options
81
+ prompts: SDKUserMessage[]
82
+ account_checked_before_prompt: boolean
83
+ interrupted: boolean
84
+ closed: boolean
85
+ }
86
+
87
+ /**
88
+ * A fake `query`: it answers `accountInfo()` with `account`, waits for the
89
+ * first prompt, then plays `messages` (a function throws where it sits).
90
+ * With `hang`, it runs until it is closed. With `turns`, it plays one list
91
+ * per prompt instead, like a session in streaming-input mode, and runs until
92
+ * it is closed. `during` runs after each prompt arrives and before that
93
+ * turn's messages play, the way the agent's tool calls happen mid-turn.
94
+ */
95
+ export const fakeQuery = ({
96
+ account,
97
+ messages,
98
+ hang,
99
+ turns,
100
+ during,
101
+ }: {
102
+ account?: unknown
103
+ messages?: (unknown | (() => never))[]
104
+ hang?: boolean
105
+ turns?: unknown[][]
106
+ during?: (call: FakeCall, turn: number) => Promise<void>
107
+ }) => {
108
+ const calls: FakeCall[] = []
109
+ const query: AgentQuery = ({ prompt, options }) => {
110
+ let accountChecked = false
111
+ const call: FakeCall = {
112
+ options,
113
+ prompts: [],
114
+ account_checked_before_prompt: false,
115
+ interrupted: false,
116
+ closed: false,
117
+ }
118
+ calls.push(call)
119
+ let wake = () => {}
120
+ const woken = new Promise<void>((resolve) => {
121
+ wake = resolve
122
+ })
123
+ let end = () => {}
124
+ const ended = new Promise<void>((resolve) => {
125
+ end = resolve
126
+ })
127
+ let notify = () => {}
128
+ const prompted = (count: number) =>
129
+ new Promise<void>((resolve) => {
130
+ notify = () => {
131
+ if (call.prompts.length >= count || call.closed) resolve()
132
+ }
133
+ notify()
134
+ })
135
+ void (async () => {
136
+ for await (const message of prompt) {
137
+ if (call.prompts.length === 0) {
138
+ call.account_checked_before_prompt = accountChecked
139
+ }
140
+ call.prompts.push(message)
141
+ wake()
142
+ notify()
143
+ }
144
+ })()
145
+ return {
146
+ [Symbol.asyncIterator]: async function* () {
147
+ if (turns !== undefined) {
148
+ for (const [index, batch] of turns.entries()) {
149
+ await prompted(index + 1)
150
+ if (call.closed) return
151
+ await during?.(call, index)
152
+ for (const message of batch) yield message
153
+ }
154
+ await ended
155
+ return
156
+ }
157
+ await woken
158
+ if (call.closed) return
159
+ await during?.(call, 0)
160
+ for (const message of messages ?? []) {
161
+ if (typeof message === 'function') message()
162
+ yield message
163
+ }
164
+ if (hang) await ended
165
+ },
166
+ accountInfo: async () => {
167
+ accountChecked = true
168
+ return account ?? MAX_ACCOUNT
169
+ },
170
+ interrupt: async () => {
171
+ call.interrupted = true
172
+ },
173
+ close: () => {
174
+ call.closed = true
175
+ wake()
176
+ end()
177
+ notify()
178
+ },
179
+ }
180
+ }
181
+ return { query, calls }
182
+ }
183
+
184
+ /** The git repo the current test runs in: a fresh one for each test. */
185
+ export let repo = ''
186
+
187
+ /** Gives each test of the file that calls it a fresh git repo in `repo`. */
188
+ export const repoPerTest = () => {
189
+ beforeEach(async () => {
190
+ repo = await mkdtemp(join(tmpdir(), 'luca-launcher-'))
191
+ await $`git init -q -b main ${repo}`.quiet()
192
+ })
193
+
194
+ afterEach(async () => {
195
+ await rm(repo, { recursive: true, force: true })
196
+ })
197
+ }
198
+
199
+ export const launch = ({
200
+ query,
201
+ model,
202
+ role,
203
+ turn_timeout_ms,
204
+ messaging,
205
+ }: {
206
+ query: AgentQuery
207
+ model?: string
208
+ role?: AgentRole
209
+ turn_timeout_ms?: number
210
+ messaging?: AgentMessaging
211
+ }): Promise<AgentTurn> =>
212
+ createClaudeLauncher({
213
+ query,
214
+ claude_path: CLAUDE_PATH,
215
+ turn_timeout_ms,
216
+ }).launch({
217
+ role: role ?? 'ticket-reviewer',
218
+ ticket: 11,
219
+ prompt: 'Review ticket #11.',
220
+ cwd: repo,
221
+ may_edit_tests: role === 'test-writer',
222
+ config: CONFIG,
223
+ messaging: messaging ?? null,
224
+ model: model ?? CLAUDE_MODEL,
225
+ })
226
+
227
+ export const IMPLEMENTER_DONE = {
228
+ outcome: 'done',
229
+ bad_test: null,
230
+ summary: 'Done.',
231
+ assumptions: [],
232
+ run_notes: [],
233
+ }