@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,403 @@
1
+ import type { BoardRecord } from './board-vocabulary'
2
+ import {
3
+ COMMITTED,
4
+ event,
5
+ finalFinishedText,
6
+ finalStartedText,
7
+ finishedText,
8
+ ignoredText,
9
+ learnerFinishedText,
10
+ lowerFirst,
11
+ messageText,
12
+ nothingToDoText,
13
+ on,
14
+ recalledText,
15
+ savedText,
16
+ SHIPPED_TEXT,
17
+ startedText,
18
+ } from './record-texts'
19
+ import {
20
+ failureText,
21
+ finalReasonText,
22
+ LEARNER,
23
+ reasonText,
24
+ RETRY_REFUSED_TEXT,
25
+ rebasedText,
26
+ runReasonText,
27
+ roleWords,
28
+ shortSha,
29
+ testsSentBackText,
30
+ } from './reduce-board'
31
+
32
+ import type { Tone } from '../shared/board-rows'
33
+ import {
34
+ LOOP_CAP,
35
+ stoppedText,
36
+ windowWords,
37
+ type BoardState,
38
+ type FindingCounts,
39
+ } from '../shared/board-state'
40
+
41
+ const findingsText = ({ findings }: { findings: FindingCounts }): string => {
42
+ const parts = [
43
+ findings.blocker > 0 ? `${findings.blocker} blocker` : null,
44
+ findings.should_fix > 0 ? `${findings.should_fix} should-fix` : null,
45
+ findings.nit > 0 ? `${findings.nit} nit` : null,
46
+ ].filter((part) => part !== null)
47
+ return parts.length > 0 ? parts.join(', ') : 'no findings'
48
+ }
49
+
50
+ /**
51
+ * What a record means in one line, or `null` for records that are noise in
52
+ * a chat (snapshots, session summaries, a clean leftover scan, the decision
53
+ * model's asks in shadow mode, ...).
54
+ */
55
+ export const describeRecord = ({
56
+ before,
57
+ after,
58
+ record,
59
+ }: {
60
+ before: BoardState
61
+ after: BoardState
62
+ record: BoardRecord
63
+ }): { text: string; tone: Tone } | null => {
64
+ const at = on({ ticket: record.ticket })
65
+ switch (record.kind) {
66
+ case 'run_started':
67
+ return event({
68
+ text: `The run started on spec #${record.content.spec_number}.`,
69
+ tone: 'info',
70
+ })
71
+ // Only a resume on a different Luca version is worth a row.
72
+ case 'engine_resumed':
73
+ return after.run.version_note !== null &&
74
+ after.run.version_note !== before.run.version_note
75
+ ? event({ text: after.run.version_note, tone: 'warning' })
76
+ : null
77
+ case 'config_reloaded':
78
+ return event({
79
+ text: `Config reloaded: ${record.content.changes.map(({ field }) => field).join(', ')} changed.`,
80
+ tone: 'info',
81
+ })
82
+ case 'decision_model_off':
83
+ return event({
84
+ text: `Decision model off: ${record.content.detail}`,
85
+ tone: 'info',
86
+ })
87
+ case 'config_reload_refused':
88
+ return event({
89
+ text: `Config not reloaded, so the run keeps its own: ${record.content.reason.split('\n')[0] ?? ''}`,
90
+ tone: 'warning',
91
+ })
92
+ case 'intake_refused':
93
+ return event({
94
+ text: `Intake refused the run. ${after.run.refusal.join('; ')}`,
95
+ tone: 'danger',
96
+ })
97
+ case 'nothing_to_do':
98
+ return event({
99
+ text: nothingToDoText({ content: record.content }),
100
+ tone: 'info',
101
+ })
102
+ case 'spec_snapshot': {
103
+ const { spec, ticket_order, left_out } = record.content
104
+ const people =
105
+ left_out.length === 0
106
+ ? ''
107
+ : ` ${left_out.length} left for a person: ${left_out.map(({ number }) => `#${number}`).join(', ')}.`
108
+ return event({
109
+ text: `Intake passed: spec #${spec.number}, ${ticket_order.length} tickets.${people}`,
110
+ tone: 'success',
111
+ })
112
+ }
113
+ case 'run_branch_created':
114
+ return event({
115
+ text: `The run branch ${record.content.branch} is ready.`,
116
+ tone: 'info',
117
+ })
118
+ case 'ticket_worktree_created':
119
+ return event({ text: `${at}started.`, tone: 'info' })
120
+ case 'usage_line_wait_started': {
121
+ const { line, percent, resets_at } = record.content
122
+ const window = windowWords({ window: record.content.window })
123
+ return event({
124
+ text: `Paused at the ${window} usage line: the account's ${window} window is at ${percent}% (line ${line}%). The run carries on when it resets at ${resets_at}, or once the line is raised.`,
125
+ tone: 'warning',
126
+ })
127
+ }
128
+ case 'usage_line_wait_ended':
129
+ return event({
130
+ text: 'The run carries on from the usage line.',
131
+ tone: 'info',
132
+ })
133
+ case 'dependencies_installed': {
134
+ const { check } = record.content
135
+ if (check === null || check.ok) return null
136
+ const where =
137
+ record.ticket === null
138
+ ? "The run branch's install"
139
+ : `${at}the install`
140
+ return event({
141
+ text: `${where} failed: \`${check.command}\`.`,
142
+ tone: 'danger',
143
+ })
144
+ }
145
+ case 'agent_started':
146
+ if (record.content.role === LEARNER) {
147
+ return event({
148
+ text: "The learner reads the run's journal for lessons to keep.",
149
+ tone: 'info',
150
+ })
151
+ }
152
+ return record.ticket === null
153
+ ? finalStartedText({ record })
154
+ : startedText({ before, after, record })
155
+ case 'agent_finished':
156
+ if (record.content.role === LEARNER) {
157
+ return learnerFinishedText({ record })
158
+ }
159
+ return record.ticket === null
160
+ ? finalFinishedText({ record })
161
+ : finishedText({ before, record })
162
+ case 'agent_failed': {
163
+ const { role, error, failure } = record.content
164
+ const who =
165
+ role === LEARNER
166
+ ? 'The learner'
167
+ : record.ticket === null
168
+ ? `Final review: the ${roleWords({ role })}`
169
+ : `${at}the ${role}`
170
+ return event({
171
+ text: `${who} ${failureText({ failure })}: ${error.split('\n')[0] ?? ''}`,
172
+ tone: failure === 'engine' ? 'warning' : 'danger',
173
+ })
174
+ }
175
+ case 'run_stopped': {
176
+ const text = stoppedText(record.content)
177
+ return event({
178
+ text: `${at}${text.charAt(0).toLowerCase()}${text.slice(1)}`,
179
+ tone: 'danger',
180
+ })
181
+ }
182
+ case 'worktree_reset':
183
+ return event({
184
+ text: `${at}starting over from the tests after a bad test.`,
185
+ tone: 'warning',
186
+ })
187
+ case 'red_check': {
188
+ const failing = after.tickets.find(
189
+ (ticket) => ticket.number === record.ticket
190
+ )?.tests?.failing
191
+ return record.content.ok
192
+ ? event({
193
+ text: `${at}red check passed${failing ? ` (${failing} new tests fail)` : ''}.`,
194
+ tone: 'success',
195
+ })
196
+ : event({
197
+ text: `${at}red check failed: ${record.content.problems[0] ?? 'no detail'}`,
198
+ tone: 'danger',
199
+ })
200
+ }
201
+ case 'leftover_scan':
202
+ return record.content.hits.length === 0
203
+ ? null
204
+ : event({
205
+ text: `${record.ticket === null ? 'Final review: ' : at}the leftover scan found ${record.content.hits.map((hit) => hit.path).join(', ')}.`,
206
+ tone: 'warning',
207
+ })
208
+ case 'commit_made':
209
+ return record.ticket === null
210
+ ? event({
211
+ text: 'Final review: the fixes committed on the run branch.',
212
+ tone: 'info',
213
+ })
214
+ : event({
215
+ text: `${at}${COMMITTED[record.content.stage] ?? 'tests'} committed.`,
216
+ tone: 'info',
217
+ })
218
+ case 'gates_run': {
219
+ const failed = record.content.checks
220
+ .filter((check) => !check.ok)
221
+ .map((check) => check.name)
222
+ // The final review's slow tests, after round 1's lenses (#563).
223
+ if (record.content.target === 'final_slow') {
224
+ return record.content.ok
225
+ ? event({
226
+ text: 'Final review: slow tests passed.',
227
+ tone: 'success',
228
+ })
229
+ : event({
230
+ text: `Final review: slow tests failed: ${failed.join(', ') || 'no detail'}.`,
231
+ tone: 'danger',
232
+ })
233
+ }
234
+ const where =
235
+ record.ticket === null
236
+ ? ' on the fixes'
237
+ : record.content.target === 'run_branch'
238
+ ? ' after joining'
239
+ : ''
240
+ const who = record.ticket === null ? 'Final review: ' : at
241
+ return record.content.ok
242
+ ? event({
243
+ text: `${who}checks passed${where}.`,
244
+ tone: 'success',
245
+ })
246
+ : event({
247
+ text: `${who}checks failed${where}: ${failed.join(', ') || 'no detail'}.`,
248
+ tone: 'danger',
249
+ })
250
+ }
251
+ case 'ticket_joined':
252
+ return record.content.ok
253
+ ? event({
254
+ text: `${at}joined the run branch.`,
255
+ tone: 'success',
256
+ })
257
+ : event({
258
+ text: `${at}couldn't join the run branch: ${record.content.error ?? 'no detail'}`,
259
+ tone: 'danger',
260
+ })
261
+ case 'ticket_rebased':
262
+ return event({
263
+ text: `${at}${lowerFirst({ text: rebasedText({ content: record.content }) })}.`,
264
+ tone: 'warning',
265
+ })
266
+ case 'tests_sent_back':
267
+ return event({
268
+ text: `${at}${lowerFirst({ text: testsSentBackText({ content: record.content }) })}.`,
269
+ tone: 'warning',
270
+ })
271
+ case 'ticket_stuck':
272
+ return event({
273
+ text: `${at}stuck. ${reasonText({ reason: record.content.reason })}`,
274
+ tone: 'danger',
275
+ })
276
+ case 'run_stuck':
277
+ return event({
278
+ text: `The run is stuck. ${runReasonText({ reason: record.content.reason })}`,
279
+ tone: 'danger',
280
+ })
281
+ case 'pull_request_opened':
282
+ return event({
283
+ text: `Pull request #${record.content.number} opened: ${record.content.url}`,
284
+ tone: 'success',
285
+ })
286
+ case 'reply_received': {
287
+ const ticket = record.content.ticket ?? record.ticket
288
+ return event({
289
+ text: `You replied \`${record.content.word}${ticket === null ? '' : ` #${ticket}`}\`.`,
290
+ tone: 'info',
291
+ })
292
+ }
293
+ case 'ticket_already_done':
294
+ return event({
295
+ text: `${at}already done on the base branch, by ${record.content.shas.map((sha) => shortSha({ sha })).join(', ')}. Nothing to build.`,
296
+ tone: 'success',
297
+ })
298
+ case 'ticket_closed':
299
+ return event({
300
+ text: `${at}closed, since its work was already done.`,
301
+ tone: 'info',
302
+ })
303
+ case 'ticket_skipped': {
304
+ const { because } = record.content
305
+ return event({
306
+ text:
307
+ because === null
308
+ ? `${at}skipped. It stays open for a later run.`
309
+ : `${at}skipped, since it waits on #${because}, which was skipped. It stays open for a later run.`,
310
+ tone: 'info',
311
+ })
312
+ }
313
+ case 'ticket_retried':
314
+ switch (record.content.mode) {
315
+ case 'resume':
316
+ return null
317
+ case 'restart':
318
+ return event({
319
+ text: `${at}starts over from the edited ticket.`,
320
+ tone: 'info',
321
+ })
322
+ case 'refused':
323
+ return event({
324
+ text: `${at}the retry was refused: ${lowerFirst({ text: RETRY_REFUSED_TEXT })} ${record.content.problems.join(' ')}`.trimEnd(),
325
+ tone: 'warning',
326
+ })
327
+ }
328
+ break
329
+ case 'reply_ignored':
330
+ return event({
331
+ text: `Your reply was sent back: ${ignoredText({ reason: record.content.reason, before })}. The answer is on the spec issue.`,
332
+ tone: 'warning',
333
+ })
334
+ case 'final_review_started':
335
+ return event({
336
+ text: `The final review started (round ${after.final_review.round}).`,
337
+ tone: 'info',
338
+ })
339
+ case 'lens_finished':
340
+ return event({
341
+ text: `Final review, ${record.content.lens} lens: ${findingsText({ findings: record.content.findings })}.`,
342
+ tone:
343
+ record.content.findings.blocker +
344
+ record.content.findings.should_fix >
345
+ 0
346
+ ? 'warning'
347
+ : 'success',
348
+ })
349
+ case 'final_review_fixing':
350
+ return event({
351
+ text: `Final review: fix round ${record.content.round}/${LOOP_CAP}.`,
352
+ tone: 'warning',
353
+ })
354
+ case 'final_review_stuck':
355
+ return event({
356
+ text: `The final review is stuck. ${finalReasonText({ reason: record.content.reason })}`,
357
+ tone: 'danger',
358
+ })
359
+ case 'final_review_passed':
360
+ return event({ text: 'The final review passed.', tone: 'success' })
361
+ case 'agent_message':
362
+ return messageText({ record })
363
+ case 'final_review_shipped':
364
+ return event({ text: SHIPPED_TEXT, tone: 'info' })
365
+ case 'memory_recalled':
366
+ return recalledText({ record })
367
+ case 'memories_saved':
368
+ return savedText({ record })
369
+ case 'learning_skipped':
370
+ return event({
371
+ text: `The learner was skipped: ${record.content.reason.split('\n')[0] ?? ''} The run ends without new memories.`,
372
+ tone: 'warning',
373
+ })
374
+ case 'memories_reported':
375
+ return event({
376
+ text: `The ${record.content.count} new ${record.content.count === 1 ? 'memory went' : 'memories went'} on the spec issue, since there is no PR.`,
377
+ tone: 'info',
378
+ })
379
+ // The repo wait shows as a banner on the panel (#632).
380
+ case 'repo_wait_started':
381
+ case 'repo_wait_ended':
382
+ case 'intake_read':
383
+ case 'ticket_snapshot':
384
+ case 'baseline_tests':
385
+ case 'baseline_reused':
386
+ case 'run_branch_pushed':
387
+ case 'worktrees_removed':
388
+ case 'agent_session':
389
+ case 'jev_asked':
390
+ case 'jev_answered':
391
+ case 'jev_failed':
392
+ case 'agent_message_delivered':
393
+ case 'shared_git_changed':
394
+ case 'limit_wait_started':
395
+ case 'limit_wait_ended':
396
+ case 'usage_recorded':
397
+ case 'lens_started':
398
+ case 'step_started':
399
+ case 'step_ended':
400
+ case 'ticket_waiting':
401
+ return null
402
+ }
403
+ }
@@ -10,25 +10,42 @@ import type { EngineSettings } from '../shared/engine-settings'
10
10
  * `import.meta.url` is undefined and the cwd is `/`.
11
11
  */
12
12
 
13
- /** What a run builds: one spec, or the engine's safe practice run. */
13
+ /** What a new run builds: one spec, or the engine's safe practice run. */
14
14
  export type RunTarget = { kind: 'spec'; spec: number } | { kind: 'demo' }
15
15
 
16
+ /** What `/luca-run` asks for: a new run, or to resume a run the board started. */
17
+ export type RunRequest = RunTarget | { kind: 'resume'; run_id: string }
18
+
16
19
  /**
17
- * Reads what followed `/luca-run`: `123`, `#123`, or `demo`.
20
+ * Reads what followed `/luca-run`: `123`, `#123`, `demo`, or
21
+ * `resume <run id>`.
18
22
  *
19
23
  * @example
20
24
  * parseRunArgs({ args: '#12' }) // { ok: true, target: { kind: 'spec', spec: 12 } }
25
+ * parseRunArgs({ args: 'resume luca-20260928-124805-69hp' }) // { ok: true, target: { kind: 'resume', run_id: 'luca-20260928-124805-69hp' } }
21
26
  * parseRunArgs({ args: 'soon' }).ok // false
22
27
  */
23
28
  export const parseRunArgs = ({
24
29
  args,
25
30
  }: {
26
31
  args: string
27
- }): { ok: true; target: RunTarget } | { ok: false; message: string } => {
32
+ }): { ok: true; target: RunRequest } | { ok: false; message: string } => {
28
33
  const text = args.trim()
29
34
  if (text.toLowerCase() === 'demo') {
30
35
  return { ok: true, target: { kind: 'demo' } }
31
36
  }
37
+ const words = text.split(/\s+/)
38
+ if (words[0]?.toLowerCase() === 'resume') {
39
+ const run_id = words[1]
40
+ return run_id !== undefined &&
41
+ words.length === 2 &&
42
+ !run_id.startsWith('-')
43
+ ? { ok: true, target: { kind: 'resume', run_id } }
44
+ : {
45
+ ok: false,
46
+ message: `Which run? Give its id, as the board shows it. Usage: ${RUN_USAGE}`,
47
+ }
48
+ }
32
49
  const match = /^#?(\d+)$/.exec(text)
33
50
  const spec = match?.[1] ? Number(match[1]) : 0
34
51
  if (spec > 0 && Number.isSafeInteger(spec)) {
@@ -0,0 +1,105 @@
1
+ import { mkdtemp } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import {
6
+ createHarness,
7
+ type Harness,
8
+ type UnfinishedRun,
9
+ } from './testing/board-harness'
10
+ import { runStarted } from './testing/journal-fixtures'
11
+
12
+ import { ROW_KIND } from '../shared/board-rows'
13
+
14
+ /**
15
+ * Helpers shared by the engine restart tests (`engine-restart.test.ts` and
16
+ * the `engine-restart-*.test.ts` files beside it).
17
+ *
18
+ * The boards and folders a test makes are in `harnesses` and `dirs`; each
19
+ * test file cleans them up after every test.
20
+ */
21
+
22
+ export const harnesses: Harness[] = []
23
+ export const dirs: string[] = []
24
+
25
+ /** A plugin on a registry kept in `registry_dir`, so it can be restarted. */
26
+ export const plugin = async ({
27
+ registry_dir,
28
+ ...options
29
+ }: {
30
+ registry_dir: string
31
+ } & Omit<Parameters<typeof createHarness>[0] & object, 'registry_dir'>) => {
32
+ const harness = await createHarness({ registry_dir, ...options })
33
+ harnesses.push(harness)
34
+ return harness
35
+ }
36
+
37
+ export const registryDir = async () => {
38
+ const dir = await mkdtemp(join(tmpdir(), 'luca-board-restart-'))
39
+ dirs.push(dir)
40
+ return dir
41
+ }
42
+
43
+ /** Runs started by a first plugin, whose engines sent `run_started`. */
44
+ export const startRuns = async ({
45
+ registry_dir,
46
+ runs,
47
+ }: {
48
+ registry_dir: string
49
+ runs: { args: string; cwd?: string }[]
50
+ }) => {
51
+ const first = await plugin({ registry_dir })
52
+ const started = []
53
+ for (const { args, cwd } of runs) {
54
+ const run = await first.start({ args, cwd })
55
+ await first.send({
56
+ run_id: run.run_id,
57
+ token: run.token,
58
+ entries: [runStarted({ spec: 10 })],
59
+ })
60
+ started.push(run)
61
+ }
62
+ return started
63
+ }
64
+
65
+ export const listed = ({
66
+ run_id,
67
+ restart = true,
68
+ reason = 'resumable',
69
+ message = null,
70
+ }: Partial<UnfinishedRun> & { run_id: string }): UnfinishedRun => ({
71
+ run_id,
72
+ restart,
73
+ reason,
74
+ message,
75
+ })
76
+
77
+ export const registryEntry = async ({
78
+ registry_dir,
79
+ run_id,
80
+ }: {
81
+ registry_dir: string
82
+ run_id: string
83
+ }) => {
84
+ const file = JSON.parse(
85
+ await Bun.file(join(registry_dir, 'runs.json')).text()
86
+ ) as { runs: { run_id: string; restarts: number; ended: unknown }[] }
87
+ return file.runs.find((entry) => entry.run_id === run_id)
88
+ }
89
+
90
+ export const runOf = async ({
91
+ harness,
92
+ run_id,
93
+ }: {
94
+ harness: Harness
95
+ run_id: string
96
+ }) => {
97
+ const { selected } = await harness.read({ run_id })
98
+ if (!selected) throw new Error(`no run ${run_id}`)
99
+ return selected.run
100
+ }
101
+
102
+ export const eventTexts = ({ harness }: { harness: Harness }) =>
103
+ harness.rows.flatMap(({ row }) =>
104
+ row.kind === ROW_KIND.event ? [row.data.text] : []
105
+ )
@@ -131,6 +131,18 @@ export type StopWhy =
131
131
  | { kind: 'check_failed'; error: string }
132
132
  | { kind: 'spawn_failed'; error: string }
133
133
 
134
+ /**
135
+ * How the owner resumes run `run_id` with the board attached: the
136
+ * `/luca-run resume` slash command (see `resumeRow`). A bare
137
+ * `luca-run --resume` would run without the board's token, so the board
138
+ * would show it read-only.
139
+ *
140
+ * @example
141
+ * resumeHint({ run_id: 'r1' }) // 'type /luca-run resume r1 in a Paseo chat'
142
+ */
143
+ export const resumeHint = ({ run_id }: { run_id: string }): string =>
144
+ `type /luca-run resume ${run_id} in a Paseo chat`
145
+
134
146
  /**
135
147
  * What the board says when a run's engine is gone and the plugin won't
136
148
  * restart it. The panel and header show it as "The engine stopped: <text>".
@@ -147,22 +159,22 @@ export const engineStoppedText = ({
147
159
  run_id: string
148
160
  log_path: string
149
161
  }): string => {
150
- const resume = `luca-run --resume ${run_id}`
162
+ const resume = resumeHint({ run_id })
151
163
  switch (why.kind) {
152
164
  case 'demo':
153
165
  return `its process is gone, and a demo can't be picked up again. Start a new one with /luca-run demo. Its log is ${log_path}.`
154
166
  case 'not_listed':
155
167
  return `its process is gone, and its journal has nothing to pick up again (it never wrote one, or the run already ended). Read its log: ${log_path}.`
156
168
  case 'launcher_stopped':
157
- return `The run stopped: ${why.reason}. It isn't restarted automatically, because it would stop the same way. Fix that, then run ${resume}. Its log is ${log_path}.`
169
+ return `The run stopped: ${why.reason}. It isn't restarted automatically, because it would stop the same way. Fix that, then ${resume}. Its log is ${log_path}.`
158
170
  case 'billing_stopped':
159
171
  return `The run stopped for billing: ${why.reason}. It will not go on; start a new run once per-token billing is off.`
160
172
  case 'restarts_used_up':
161
- return `it stopped again after ${MAX_AUTO_RESTARTS} automatic restarts, so Paseo stopped restarting it. Read its log: ${log_path}. To try once more, run ${resume}.`
173
+ return `it stopped again after ${MAX_AUTO_RESTARTS} automatic restarts, so Paseo stopped restarting it. Read its log: ${log_path}. To try once more, ${resume}.`
162
174
  case 'check_failed':
163
- return `its process is gone, and Paseo couldn't check whether the run can go on (${why.error}). To go on from its journal, run ${resume}. Its log is ${log_path}.`
175
+ return `its process is gone, and Paseo couldn't check whether the run can go on (${why.error}). To go on from its journal, ${resume}. Its log is ${log_path}.`
164
176
  case 'spawn_failed':
165
- return `its process is gone, and Paseo couldn't restart the engine: ${why.error}. To go on from its journal, run ${resume}. Its log is ${log_path}.`
177
+ return `its process is gone, and Paseo couldn't restart the engine: ${why.error}. To go on from its journal, ${resume}. Its log is ${log_path}.`
166
178
  }
167
179
  }
168
180
 
@@ -195,3 +207,31 @@ export const restartRow = ({
195
207
  tone: 'warning',
196
208
  },
197
209
  })
210
+
211
+ /** The words of the chat row for a run resumed with `/luca-run resume`. */
212
+ export const RESUMED_TEXT =
213
+ 'The run was resumed from its journal with /luca-run resume.'
214
+
215
+ /**
216
+ * The chat row for a run resumed with `/luca-run resume`; its id is unique
217
+ * per run and time, so each resume gets its own row.
218
+ *
219
+ * @example
220
+ * resumeRow({ run_id, time: new Date().toISOString() })
221
+ */
222
+ export const resumeRow = ({
223
+ run_id,
224
+ time,
225
+ }: {
226
+ run_id: string
227
+ time: string
228
+ }): BoardRow => ({
229
+ id: `${run_id}-resumed-${time}`,
230
+ kind: ROW_KIND.event,
231
+ data: {
232
+ time,
233
+ ticket: null,
234
+ text: RESUMED_TEXT,
235
+ tone: 'info',
236
+ },
237
+ })