@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,129 @@
1
+ /**
2
+ * Running the engine for a run: the environment it gets, finding it from
3
+ * the settings, the one-at-a-time lock on engine work, and spawning it with
4
+ * `--resume`.
5
+ */
6
+ import type { RunStore } from './board-server-store'
7
+ import type { BoardServerConfig, RunMemory } from './board-server-types'
8
+ import { errorText } from './board-server-util'
9
+ import { resolveEngine } from './engine-launch'
10
+ import { engineStoppedText, resumeArgs } from './engine-watch'
11
+
12
+ import {
13
+ EngineSettingsSchema,
14
+ type EngineSettings,
15
+ } from '../shared/engine-settings'
16
+
17
+ export const createEngineProcess = ({
18
+ config,
19
+ store,
20
+ }: {
21
+ config: BoardServerConfig
22
+ store: RunStore
23
+ }) => {
24
+ const {
25
+ env,
26
+ read_settings,
27
+ log,
28
+ home_dir,
29
+ file_exists,
30
+ real_path,
31
+ spawn_engine,
32
+ } = config
33
+ const { enqueue, endRun } = store
34
+
35
+ /** The daemon's env for the engine, plus the run's token when given. */
36
+ const childEnv = ({
37
+ token,
38
+ }: {
39
+ token: string | null
40
+ }): Record<string, string> => {
41
+ const child_env: Record<string, string> = {}
42
+ for (const [key, value] of Object.entries(env)) {
43
+ if (value !== undefined) child_env[key] = value
44
+ }
45
+ if (token !== null) child_env.LUCA_BOARD_TOKEN = token
46
+ return child_env
47
+ }
48
+
49
+ /** Finds the engine from the current settings, as `run.start` does. */
50
+ const findEngine = async () => {
51
+ let settings: EngineSettings = EngineSettingsSchema.parse({})
52
+ try {
53
+ settings = await read_settings()
54
+ } catch (error) {
55
+ log(`Couldn't read the engine settings: ${errorText({ error })}`)
56
+ }
57
+ return resolveEngine({
58
+ settings,
59
+ env,
60
+ home_dir,
61
+ file_exists,
62
+ real_path,
63
+ })
64
+ }
65
+
66
+ /**
67
+ * Engine checks and resumes run one at a time, so a resume and a check
68
+ * never both start an engine for the same run.
69
+ */
70
+ let engine_work: Promise<unknown> = Promise.resolve()
71
+ const exclusive = <Result>(work: () => Promise<Result>) => {
72
+ const result = engine_work.then(work)
73
+ engine_work = result.catch(() => undefined)
74
+ return result
75
+ }
76
+
77
+ /** Spawns a run's engine with `--resume` and its token; returns its pid. */
78
+ const spawnResume = ({
79
+ memory,
80
+ engine,
81
+ on_failed,
82
+ }: {
83
+ memory: RunMemory
84
+ engine: { command: string; lead_args: string[] }
85
+ /** What the log says when the process fails after the spawn. */
86
+ on_failed: string
87
+ }): number | null => {
88
+ const { run_id, repo, token, log_path } = memory.entry
89
+ return spawn_engine({
90
+ command: engine.command,
91
+ args: [...engine.lead_args, ...resumeArgs({ run_id, repo })],
92
+ env: childEnv({ token }),
93
+ cwd: repo,
94
+ log_path,
95
+ on_error: (error) => {
96
+ log(`[${run_id}] ${on_failed}: ${errorText({ error })}`)
97
+ void enqueue({
98
+ run_id,
99
+ work: () =>
100
+ endRun({
101
+ memory,
102
+ ended: {
103
+ ok: false,
104
+ message: engineStoppedText({
105
+ why: {
106
+ kind: 'spawn_failed',
107
+ error: errorText({ error }),
108
+ },
109
+ run_id,
110
+ log_path,
111
+ }),
112
+ },
113
+ }),
114
+ })
115
+ },
116
+ }).pid
117
+ }
118
+
119
+ return {
120
+ childEnv,
121
+ findEngine,
122
+ exclusive,
123
+ spawnResume,
124
+ /** The engine work in flight (or done), to wait for. */
125
+ working: () => engine_work,
126
+ }
127
+ }
128
+
129
+ export type EngineProcess = ReturnType<typeof createEngineProcess>
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `engine.event`: applies a run's journal records, sent by the engine, in
3
+ * seq order, and adds the rows they make.
4
+ */
5
+ import { timingSafeEqual } from 'node:crypto'
6
+
7
+ import type { RunStore } from './board-server-store'
8
+ import type { RunMemory } from './board-server-types'
9
+ import { applyEnded } from './reduce-board'
10
+
11
+ import type { BoardRow } from '../shared/board-rows'
12
+ import type {
13
+ EngineEventInput,
14
+ EngineEventOutput,
15
+ EngineRecord,
16
+ } from '../shared/board-rpc'
17
+
18
+ const sameToken = ({ left, right }: { left: string; right: string }) => {
19
+ const a = Buffer.from(left)
20
+ const b = Buffer.from(right)
21
+ return a.length === b.length && timingSafeEqual(a, b)
22
+ }
23
+
24
+ export const createEngineEvents = ({ store }: { store: RunStore }) => {
25
+ const {
26
+ runs,
27
+ enqueue,
28
+ applyInOrder,
29
+ appendRows,
30
+ header,
31
+ updateEntry,
32
+ ready,
33
+ } = store
34
+
35
+ /**
36
+ * The kept rows of records rebuilt from disk that the engine sent again.
37
+ * The engine sends from the first record it hasn't had an answer for, so
38
+ * the chat already has the rows of every lower seq: those are dropped.
39
+ */
40
+ const takeUnsentRows = ({
41
+ memory,
42
+ records,
43
+ }: {
44
+ memory: RunMemory
45
+ records: EngineRecord[]
46
+ }): { rows: BoardRow[]; taken: number } => {
47
+ const rows: BoardRow[] = []
48
+ let taken = 0
49
+ if (memory.unsent_rows.size === 0 || records.length === 0) {
50
+ return { rows, taken }
51
+ }
52
+ const lowest = Math.min(...records.map((record) => record.seq))
53
+ const sent = new Set(records.map((record) => record.seq))
54
+ for (const [seq, seq_rows] of memory.unsent_rows) {
55
+ if (seq < lowest) {
56
+ memory.unsent_rows.delete(seq)
57
+ } else if (sent.has(seq)) {
58
+ rows.push(...seq_rows)
59
+ memory.unsent_rows.delete(seq)
60
+ taken += 1
61
+ }
62
+ }
63
+ return { rows, taken }
64
+ }
65
+
66
+ /**
67
+ * `engine.event`: applies a run's journal records in seq order from its
68
+ * `next_seq`. Lower seqs are duplicates and skipped (but a record rebuilt
69
+ * from disk adds its rows the first time the engine sends it); a gap
70
+ * stops there and the reply's `next_seq` asks the engine to resend from it.
71
+ */
72
+ const handleEngineEvent = async (
73
+ input: EngineEventInput
74
+ ): Promise<EngineEventOutput> => {
75
+ await ready
76
+ // A run this plugin didn't start is read-only: it isn't in `runs`.
77
+ const memory = runs.get(input.run_id)
78
+ if (!memory) {
79
+ return {
80
+ ok: false,
81
+ next_seq: 0,
82
+ message: `The board doesn't know run ${input.run_id}.`,
83
+ }
84
+ }
85
+ if (!sameToken({ left: input.token, right: memory.entry.token })) {
86
+ return {
87
+ ok: false,
88
+ next_seq: 0,
89
+ message: `Wrong token for run ${input.run_id}.`,
90
+ }
91
+ }
92
+ return enqueue({
93
+ run_id: input.run_id,
94
+ work: async () => {
95
+ const unsent = takeUnsentRows({
96
+ memory,
97
+ records: input.records,
98
+ })
99
+ const {
100
+ rows: new_rows,
101
+ applied,
102
+ gap,
103
+ } = applyInOrder({
104
+ memory,
105
+ records: input.records,
106
+ })
107
+ const rows = [...unsent.rows, ...[...new_rows.values()].flat()]
108
+ if (gap === null && input.ended) {
109
+ memory.state = applyEnded({
110
+ state: memory.state,
111
+ ended: input.ended,
112
+ })
113
+ memory.unsent_rows.clear()
114
+ updateEntry({ memory, change: { ended: input.ended } })
115
+ }
116
+ if (
117
+ unsent.taken > 0 ||
118
+ applied > 0 ||
119
+ (gap === null && input.ended)
120
+ ) {
121
+ rows.push(header({ memory }))
122
+ }
123
+ await appendRows({ memory, rows })
124
+ const message =
125
+ gap === null
126
+ ? `Applied ${applied} records.`
127
+ : `Applied ${applied} records, then found seq ${gap} while waiting for ${memory.next_seq}: resend from ${memory.next_seq}.`
128
+ return { ok: true, next_seq: memory.next_seq, message }
129
+ },
130
+ })
131
+ }
132
+
133
+ return { handleEngineEvent }
134
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * What the side panel reads and posts: `board.read` (the runs started from
3
+ * a workspace's chats and the runs the plugin didn't start, read from their
4
+ * journals), and `reply.post`.
5
+ */
6
+ import { resolve } from 'node:path'
7
+
8
+ import { z } from 'zod'
9
+
10
+ import type { RunStore } from './board-server-store'
11
+ import type { BoardServerConfig, OutsideRun } from './board-server-types'
12
+ import { readLiveFile, withLive } from './live-file'
13
+ import { createBoardState } from './reduce-board'
14
+ import type { ReplyRun } from './reply-post'
15
+ import { listJournals } from './run-journals'
16
+
17
+ import type {
18
+ BoardReadInput,
19
+ BoardReadOutput,
20
+ EngineRecord,
21
+ ReplyPostInput,
22
+ ReplyPostOutput,
23
+ } from '../shared/board-rpc'
24
+ import { needsEngine } from '../shared/board-state'
25
+
26
+ const RunRepoSchema = z.object({ repo: z.string().min(1) })
27
+
28
+ /** The repo a run's `run_started` record names, if any. */
29
+ const repoOf = ({ records }: { records: EngineRecord[] }): string | null => {
30
+ const started = records.find((record) => record.kind === 'run_started')
31
+ const parsed = RunRepoSchema.safeParse(started?.content)
32
+ return parsed.success ? resolve(parsed.data.repo) : null
33
+ }
34
+
35
+ /** How many runs the plugin didn't start it shows, the latest changed first. */
36
+ const OUTSIDE_RUNS_SHOWN = 50
37
+
38
+ export const createBoardReads = ({
39
+ config,
40
+ store,
41
+ }: {
42
+ config: BoardServerConfig
43
+ store: RunStore
44
+ }) => {
45
+ const { runs_dir = null } = config
46
+ const { runs, poster, applyInOrder, journalOf, ready } = store
47
+ const outside = new Map<string, OutsideRun>()
48
+
49
+ /**
50
+ * Reads the journals of runs this plugin didn't start, the latest
51
+ * changed first: a new or changed journal is read again and its new
52
+ * records applied.
53
+ */
54
+ const readOutside = async () => {
55
+ if (runs_dir === null) return
56
+ const journals = (await listJournals({ runs_dir }))
57
+ .filter((journal) => !runs.has(journal.run_id))
58
+ .slice(0, OUTSIDE_RUNS_SHOWN)
59
+ const shown = new Set(journals.map((journal) => journal.run_id))
60
+ for (const run_id of outside.keys()) {
61
+ if (!shown.has(run_id)) outside.delete(run_id)
62
+ }
63
+ await Promise.all(
64
+ journals.map(async ({ run_id, size, changed_ms }) => {
65
+ const known = outside.get(run_id)
66
+ if (known?.size === size && known.changed_ms === changed_ms) {
67
+ return
68
+ }
69
+ const records = await journalOf({ run_id })
70
+ const first = records[0]
71
+ if (!first) return
72
+ const run: OutsideRun = known ?? {
73
+ state: createBoardState({
74
+ run_id,
75
+ spec_number: null,
76
+ demo: false,
77
+ started_at: first.time,
78
+ log_path: null,
79
+ }),
80
+ next_seq: 1,
81
+ size,
82
+ changed_ms,
83
+ repo: null,
84
+ engine_gone: false,
85
+ }
86
+ applyInOrder({ memory: run, records })
87
+ run.size = size
88
+ run.changed_ms = changed_ms
89
+ run.repo ??= repoOf({ records })
90
+ outside.set(run_id, run)
91
+ })
92
+ )
93
+ }
94
+
95
+ /**
96
+ * `board.read`: the runs started from the workspace's chats and the runs
97
+ * this plugin didn't start whose repo is the workspace's folder, newest
98
+ * first, and one of them in full, with its live file laid over it.
99
+ */
100
+ const readBoard = async (
101
+ input: BoardReadInput
102
+ ): Promise<BoardReadOutput> => {
103
+ await ready
104
+ await readOutside()
105
+ const directory = input.directory ? resolve(input.directory) : null
106
+ const list = [
107
+ ...[...runs.values()].filter(
108
+ (memory) => memory.entry.workspace_id === input.workspace_id
109
+ ),
110
+ ...[...outside.values()]
111
+ .filter((run) => directory !== null && run.repo === directory)
112
+ .map((run) => ({
113
+ state: {
114
+ ...run.state,
115
+ engine_gone:
116
+ run.engine_gone &&
117
+ needsEngine({ state: run.state }),
118
+ },
119
+ })),
120
+ ]
121
+ .map(({ state }) => state)
122
+ .toSorted((left, right) =>
123
+ right.run.started_at === left.run.started_at
124
+ ? right.run.run_id.localeCompare(left.run.run_id)
125
+ : right.run.started_at.localeCompare(left.run.started_at)
126
+ )
127
+ const selected =
128
+ (input.run_id
129
+ ? list.find((state) => state.run.run_id === input.run_id)
130
+ : undefined) ?? list[0]
131
+ const selected_id = selected?.run.run_id ?? null
132
+ const live =
133
+ runs_dir === null || selected_id === null
134
+ ? null
135
+ : await readLiveFile({ runs_dir, run_id: selected_id })
136
+ return {
137
+ runs: list.map((state) => ({
138
+ run_id: state.run.run_id,
139
+ spec_number: state.run.spec_number,
140
+ spec_title: state.run.spec_title,
141
+ demo: state.run.demo,
142
+ status: state.run.status,
143
+ started_at: state.run.started_at,
144
+ needs_you: state.needs_you.length,
145
+ })),
146
+ selected:
147
+ selected === undefined
148
+ ? null
149
+ : withLive({ state: selected, live }),
150
+ chat_agent_id:
151
+ selected_id === null
152
+ ? null
153
+ : (runs.get(selected_id)?.entry.agent_id ?? null),
154
+ posted:
155
+ selected_id === null
156
+ ? []
157
+ : poster.postedOf({ run_id: selected_id }),
158
+ }
159
+ }
160
+
161
+ /**
162
+ * `reply.post` (#503): posts a reply button's word on the run's spec
163
+ * issue. Works for runs this plugin started and for runs it only reads
164
+ * (their journal is read again first); see `createReplyPoster`.
165
+ */
166
+ const postReply = async (
167
+ input: ReplyPostInput
168
+ ): Promise<ReplyPostOutput> => {
169
+ await ready
170
+ await readOutside()
171
+ const memory = runs.get(input.run_id)
172
+ const other = outside.get(input.run_id)
173
+ const run: ReplyRun | null = memory
174
+ ? {
175
+ state: memory.state,
176
+ repo_dir: memory.entry.repo,
177
+ next_seq: memory.next_seq,
178
+ }
179
+ : other
180
+ ? {
181
+ state: other.state,
182
+ repo_dir: other.repo,
183
+ next_seq: other.next_seq,
184
+ }
185
+ : null
186
+ return poster.postReply({ run, input })
187
+ }
188
+
189
+ return { outside, readOutside, readBoard, postReply }
190
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * `/luca-run resume <run id>` (#493): going on with a run this plugin
3
+ * started, from its journal.
4
+ */
5
+ import { randomBytes } from 'node:crypto'
6
+
7
+ import type { EngineProcess } from './board-server-engine'
8
+ import type { RunStore } from './board-server-store'
9
+ import type { BoardServerConfig } from './board-server-types'
10
+ import { errorText } from './board-server-util'
11
+ import { liveRunIds, RESUMED_TEXT, resumeRow } from './engine-watch'
12
+
13
+ import type { RunStartInput, RunStartOutput } from '../shared/board-rpc'
14
+ import { OVER_PHASES } from '../shared/board-state'
15
+
16
+ export const createResumer = ({
17
+ config,
18
+ store,
19
+ engine,
20
+ }: {
21
+ config: BoardServerConfig
22
+ store: RunStore
23
+ engine: EngineProcess
24
+ }) => {
25
+ const { list_processes, log, now } = config
26
+ const { runs, enqueue, appendRows, header, updateEntry } = store
27
+ const { findEngine, exclusive, spawnResume } = engine
28
+
29
+ /**
30
+ * `/luca-run resume <run id>` (#493): goes on with a run this plugin
31
+ * started, from its journal, with the board attached again. Refused for
32
+ * an unknown run, a demo, a run that is over, a run whose engine is
33
+ * still running, or when `ps` fails. Otherwise the run gets a new token,
34
+ * loses its `ended`, and starts its restart count again at 0; it keeps
35
+ * its chat, workspace, repo, and log. The engine is spawned with
36
+ * `--resume`, and the run's chat gets a row saying so.
37
+ */
38
+ const resumeRun = async ({
39
+ run_id,
40
+ input,
41
+ }: {
42
+ run_id: string
43
+ input: RunStartInput
44
+ }): Promise<RunStartOutput> => {
45
+ const refuse = (message: string): RunStartOutput => ({
46
+ ok: false,
47
+ message,
48
+ run_id: null,
49
+ })
50
+ const memory = runs.get(run_id)
51
+ if (!memory) {
52
+ return refuse(
53
+ `The board doesn't know run ${run_id}, so it can't resume it. Check the id: the run's header row and the board panel show it. A run the board didn't start goes on from a terminal with luca-run --resume ${run_id}, without the board.`
54
+ )
55
+ }
56
+ if (memory.entry.demo) {
57
+ return refuse(
58
+ `Run ${run_id} is a demo, and a demo can't be picked up again. Start a new one with /luca-run demo.`
59
+ )
60
+ }
61
+ if (OVER_PHASES.has(memory.state.run.phase)) {
62
+ return refuse(
63
+ `Run ${run_id} is over, so there is nothing to resume.`
64
+ )
65
+ }
66
+ const engine = await findEngine()
67
+ if (!engine.ok) return refuse(engine.message)
68
+
69
+ return exclusive(async () => {
70
+ let command_lines: string[]
71
+ try {
72
+ command_lines = await list_processes()
73
+ } catch (error) {
74
+ return refuse(
75
+ `Couldn't check whether run ${run_id}'s engine is still running (${errorText({ error })}), so it wasn't resumed: two engines on one run would be far worse. Try again.`
76
+ )
77
+ }
78
+ if (liveRunIds({ command_lines }).has(run_id)) {
79
+ return refuse(
80
+ `Run ${run_id} is still running (its engine is live), so there is nothing to resume.`
81
+ )
82
+ }
83
+ return enqueue({
84
+ run_id,
85
+ work: async (): Promise<RunStartOutput> => {
86
+ const before = memory.entry
87
+ // The new token is kept first: the engine may send
88
+ // before the spawn returns.
89
+ updateEntry({
90
+ memory,
91
+ change: {
92
+ token: randomBytes(24).toString('hex'),
93
+ ended: null,
94
+ restarts: 0,
95
+ },
96
+ })
97
+ let pid: number | null = null
98
+ try {
99
+ pid = spawnResume({
100
+ memory,
101
+ engine,
102
+ on_failed: 'the resumed engine failed',
103
+ })
104
+ } catch (error) {
105
+ updateEntry({
106
+ memory,
107
+ change: {
108
+ token: before.token,
109
+ ended: before.ended,
110
+ restarts: before.restarts,
111
+ },
112
+ })
113
+ return refuse(
114
+ `Couldn't start the engine: ${errorText({ error })}`
115
+ )
116
+ }
117
+ const { log_path, spec, agent_id } = memory.entry
118
+ log(
119
+ `[${run_id}] resumed with /luca-run resume: ${engine.command} with --resume (pid ${pid ?? '?'}), log ${log_path}`
120
+ )
121
+ memory.rows_stopped = false
122
+ memory.append_failures = 0
123
+ memory.state = {
124
+ ...memory.state,
125
+ run: { ...memory.state.run, engine_ended: null },
126
+ latest: RESUMED_TEXT,
127
+ latest_at: now().toISOString(),
128
+ }
129
+ void enqueue({
130
+ run_id,
131
+ work: () =>
132
+ appendRows({
133
+ memory,
134
+ rows: [
135
+ resumeRow({
136
+ run_id,
137
+ time: now().toISOString(),
138
+ }),
139
+ header({ memory }),
140
+ ],
141
+ }),
142
+ })
143
+ const what =
144
+ spec === null
145
+ ? `run ${run_id}`
146
+ : `spec #${spec} (run ${run_id})`
147
+ const where =
148
+ agent_id === input.agent_id
149
+ ? ''
150
+ : ' Its rows go on in the chat it started in.'
151
+ return {
152
+ ok: true,
153
+ run_id,
154
+ message: `Resumed ${what} from its journal.${where} Its log is ${log_path}.`,
155
+ }
156
+ },
157
+ })
158
+ })
159
+ }
160
+
161
+ return { resumeRun }
162
+ }
163
+
164
+ export type Resumer = ReturnType<typeof createResumer>