@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,174 @@
1
+ /**
2
+ * `run.start`: parses the args, finds the engine, records the run, spawns
3
+ * the engine detached, and adds the chat's header row.
4
+ */
5
+ import { randomBytes, randomInt } from 'node:crypto'
6
+ import { join } from 'node:path'
7
+
8
+ import type { EngineProcess } from './board-server-engine'
9
+ import type { Resumer } from './board-server-resume'
10
+ import type { RunStore } from './board-server-store'
11
+ import type { BoardServerConfig } from './board-server-types'
12
+ import { errorText } from './board-server-util'
13
+ import { engineArgs, mintRunId, parseRunArgs } from './engine-launch'
14
+ import type { RunEntry } from './run-registry'
15
+
16
+ import type { RunStartInput, RunStartOutput } from '../shared/board-rpc'
17
+
18
+ /** The longest `run.start` waits for the header row before it returns. */
19
+ const HEADER_WAIT_MS = 3000
20
+
21
+ const SUFFIX_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789'
22
+
23
+ const randomSuffix = (): string =>
24
+ Array.from(
25
+ { length: 4 },
26
+ () => SUFFIX_ALPHABET[randomInt(SUFFIX_ALPHABET.length)] ?? 'x'
27
+ ).join('')
28
+
29
+ export const createStarter = ({
30
+ config,
31
+ store,
32
+ engine,
33
+ resumer,
34
+ }: {
35
+ config: BoardServerConfig
36
+ store: RunStore
37
+ engine: EngineProcess
38
+ resumer: Resumer
39
+ }) => {
40
+ const { spawn_engine, log, now, log_dir } = config
41
+ const {
42
+ runs,
43
+ registry,
44
+ queues,
45
+ remember,
46
+ enqueue,
47
+ endRun,
48
+ appendRows,
49
+ header,
50
+ ready,
51
+ } = store
52
+ const { childEnv, findEngine } = engine
53
+ const { resumeRun } = resumer
54
+
55
+ /** Waits for rows being added, but at most `HEADER_WAIT_MS`. */
56
+ const waitForRows = async (adding: Promise<unknown>) => {
57
+ let timer: ReturnType<typeof setTimeout> | undefined
58
+ await Promise.race([
59
+ adding,
60
+ new Promise((resolve) => {
61
+ timer = setTimeout(resolve, HEADER_WAIT_MS)
62
+ }),
63
+ ])
64
+ clearTimeout(timer)
65
+ }
66
+
67
+ /**
68
+ * `run.start`: parses the args, finds the engine, records the run, spawns
69
+ * the engine detached, and adds the chat's header row. Returns at once.
70
+ * `resume <run id>` goes on with a run instead (see `resumeRun`).
71
+ */
72
+ const startRun = async (input: RunStartInput): Promise<RunStartOutput> => {
73
+ await ready
74
+ const parsed = parseRunArgs({ args: input.args })
75
+ if (!parsed.ok)
76
+ return { ok: false, message: parsed.message, run_id: null }
77
+ if (parsed.target.kind === 'resume') {
78
+ const output = await resumeRun({
79
+ run_id: parsed.target.run_id,
80
+ input,
81
+ })
82
+ const rows = queues.get(parsed.target.run_id)
83
+ if (output.ok && rows) await waitForRows(rows)
84
+ return output
85
+ }
86
+
87
+ const engine = await findEngine()
88
+ if (!engine.ok)
89
+ return { ok: false, message: engine.message, run_id: null }
90
+
91
+ let run_id = mintRunId({ now: now(), suffix: randomSuffix() })
92
+ while (runs.has(run_id)) {
93
+ run_id = mintRunId({ now: now(), suffix: randomSuffix() })
94
+ }
95
+ const token = randomBytes(24).toString('hex')
96
+ const log_path = join(log_dir, `${run_id}.log`)
97
+ const target = parsed.target
98
+ const entry: RunEntry = {
99
+ run_id,
100
+ token,
101
+ agent_id: input.agent_id,
102
+ workspace_id: input.workspace_id,
103
+ repo: input.cwd,
104
+ spec: target.kind === 'spec' ? target.spec : null,
105
+ demo: target.kind === 'demo',
106
+ started_at: now().toISOString(),
107
+ log_path,
108
+ ended: null,
109
+ restarts: 0,
110
+ }
111
+ registry.add({ entry })
112
+ const memory = remember({ entry, records: [] })
113
+
114
+ const child_env = childEnv({ token })
115
+
116
+ let pid: number | null = null
117
+ try {
118
+ pid = spawn_engine({
119
+ command: engine.command,
120
+ args: [
121
+ ...engine.lead_args,
122
+ ...engineArgs({ target, repo: input.cwd, run_id }),
123
+ ],
124
+ env: child_env,
125
+ cwd: input.cwd,
126
+ log_path,
127
+ on_error: (error) => {
128
+ log(
129
+ `[${run_id}] the engine process failed: ${errorText({ error })}`
130
+ )
131
+ void enqueue({
132
+ run_id,
133
+ work: () =>
134
+ endRun({
135
+ memory,
136
+ ended: {
137
+ ok: false,
138
+ message: `The engine couldn't start: ${errorText({ error })}`,
139
+ },
140
+ }),
141
+ })
142
+ },
143
+ }).pid
144
+ } catch (error) {
145
+ registry.remove({ run_id })
146
+ runs.delete(run_id)
147
+ return {
148
+ ok: false,
149
+ message: `Couldn't start the engine: ${errorText({ error })}`,
150
+ run_id: null,
151
+ }
152
+ }
153
+ log(
154
+ `[${run_id}] started ${engine.command} (pid ${pid ?? '?'}), log ${log_path}`
155
+ )
156
+
157
+ await waitForRows(
158
+ enqueue({
159
+ run_id,
160
+ work: () => appendRows({ memory, rows: [header({ memory })] }),
161
+ })
162
+ )
163
+
164
+ const what =
165
+ target.kind === 'demo' ? 'a demo run' : `spec #${target.spec}`
166
+ return {
167
+ ok: true,
168
+ run_id,
169
+ message: `Started ${what} as ${run_id}. Its log is ${log_path}.`,
170
+ }
171
+ }
172
+
173
+ return { startRun }
174
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * The board server's runs in memory: each run the plugin started, its board
3
+ * built from its journal records, the queue that keeps a run's work in
4
+ * order, and the rows it adds to the chat.
5
+ */
6
+ import type { BoardServerConfig, Replay, RunMemory } from './board-server-types'
7
+ import { errorText } from './board-server-util'
8
+ import { readRecord } from './board-vocabulary'
9
+ import { describeRecord, headerRow, rowsForRecord } from './make-rows'
10
+ import { applyEnded, applyRecord, createBoardState } from './reduce-board'
11
+ import { createReplyPoster } from './reply-post'
12
+ import { readJournal } from './run-journals'
13
+ import { createRunRegistry, type RunEntry } from './run-registry'
14
+
15
+ import type { BoardRow } from '../shared/board-rows'
16
+ import type { EngineRecord } from '../shared/board-rpc'
17
+ import type { EngineEnded } from '../shared/board-state'
18
+
19
+ /** After this many failed appends in a row, a run stops sending rows. */
20
+ const APPEND_FAILURES_BEFORE_STOP = 3
21
+
22
+ /**
23
+ * The runs this plugin started, their queues, and the work on them. With a
24
+ * `runs_dir`, every run in the registry is rebuilt from its journal there,
25
+ * and `ready` resolves once that is done.
26
+ */
27
+ export const createRunStore = ({ config }: { config: BoardServerConfig }) => {
28
+ const {
29
+ registry_path,
30
+ runs_dir = null,
31
+ append_row,
32
+ run_command,
33
+ env,
34
+ home_dir,
35
+ file_exists,
36
+ now,
37
+ log,
38
+ } = config
39
+ const registry = createRunRegistry({ path: registry_path, log })
40
+ const runs = new Map<string, RunMemory>()
41
+ const queues = new Map<string, Promise<unknown>>()
42
+ const poster = createReplyPoster({
43
+ run_command,
44
+ env,
45
+ home_dir,
46
+ file_exists,
47
+ now,
48
+ log,
49
+ })
50
+
51
+ /** Runs `work` after everything already queued for the same run. */
52
+ const enqueue = <Result>({
53
+ run_id,
54
+ work,
55
+ }: {
56
+ run_id: string
57
+ work: () => Promise<Result>
58
+ }): Promise<Result> => {
59
+ const result = (queues.get(run_id) ?? Promise.resolve()).then(work)
60
+ queues.set(
61
+ run_id,
62
+ result.catch(() => undefined)
63
+ )
64
+ return result
65
+ }
66
+
67
+ const appendRows = async ({
68
+ memory,
69
+ rows,
70
+ }: {
71
+ memory: RunMemory
72
+ rows: BoardRow[]
73
+ }) => {
74
+ for (const row of rows) {
75
+ if (memory.rows_stopped) return
76
+ try {
77
+ await append_row({ agent_id: memory.entry.agent_id, row })
78
+ memory.append_failures = 0
79
+ } catch (error) {
80
+ memory.append_failures += 1
81
+ log(
82
+ `[${memory.entry.run_id}] couldn't add row ${row.id} to chat ${memory.entry.agent_id}: ${errorText({ error })}`
83
+ )
84
+ if (memory.append_failures >= APPEND_FAILURES_BEFORE_STOP) {
85
+ memory.rows_stopped = true
86
+ log(
87
+ `[${memory.entry.run_id}] stopped adding rows: chat ${memory.entry.agent_id} seems gone. The side panel still updates.`
88
+ )
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ const header = ({ memory }: { memory: RunMemory }): BoardRow =>
95
+ headerRow({ state: memory.state, updated_at: now().toISOString() })
96
+
97
+ /** Applies one record to a run's board; returns the rows it makes. */
98
+ const applyOne = ({
99
+ memory,
100
+ record,
101
+ }: {
102
+ memory: Replay
103
+ record: EngineRecord
104
+ }): BoardRow[] => {
105
+ const { run_id } = memory.state.run
106
+ const read = readRecord({ record })
107
+ if (read.status !== 'known') {
108
+ if (read.status === 'bad_content') {
109
+ log(
110
+ `[${run_id}] skipped record ${record.seq} (${record.kind}): its content doesn't fit.\n${read.error}`
111
+ )
112
+ }
113
+ memory.state = {
114
+ ...memory.state,
115
+ event_count: memory.state.event_count + 1,
116
+ }
117
+ return []
118
+ }
119
+ const before = memory.state
120
+ const after = applyRecord({ state: before, record: read.record })
121
+ if (read.record.kind === 'reply_received') {
122
+ poster.noteTaken({
123
+ run_id,
124
+ seq: read.record.seq,
125
+ time: read.record.time,
126
+ word: read.record.content.word,
127
+ ticket: read.record.content.ticket ?? read.record.ticket,
128
+ })
129
+ }
130
+ const described = describeRecord({
131
+ before,
132
+ after,
133
+ record: read.record,
134
+ })
135
+ memory.state = described
136
+ ? { ...after, latest: described.text, latest_at: read.record.time }
137
+ : after
138
+ return rowsForRecord({ run_id, before, after, record: read.record })
139
+ }
140
+
141
+ /**
142
+ * Applies records in seq order from the run's `next_seq`. Lower seqs are
143
+ * duplicates and skipped; a gap stops there (`gap` is the seq found).
144
+ */
145
+ const applyInOrder = ({
146
+ memory,
147
+ records,
148
+ }: {
149
+ memory: Replay
150
+ records: EngineRecord[]
151
+ }): {
152
+ rows: Map<number, BoardRow[]>
153
+ applied: number
154
+ gap: number | null
155
+ } => {
156
+ const rows = new Map<number, BoardRow[]>()
157
+ let applied = 0
158
+ for (const record of records.toSorted(
159
+ (left, right) => left.seq - right.seq
160
+ )) {
161
+ if (record.seq < memory.next_seq) continue
162
+ if (record.seq > memory.next_seq) {
163
+ return { rows, applied, gap: record.seq }
164
+ }
165
+ rows.set(record.seq, applyOne({ memory, record }))
166
+ memory.next_seq += 1
167
+ applied += 1
168
+ }
169
+ return { rows, applied, gap: null }
170
+ }
171
+
172
+ /**
173
+ * Knows a run this plugin started, rebuilt from its journal `records`.
174
+ * Their rows aren't added now; while the engine may still send, they are
175
+ * kept until it sends those records again (see `handleEngineEvent`).
176
+ */
177
+ const remember = ({
178
+ entry,
179
+ records,
180
+ }: {
181
+ entry: RunEntry
182
+ records: EngineRecord[]
183
+ }): RunMemory => {
184
+ const memory: RunMemory = {
185
+ entry,
186
+ state: createBoardState({
187
+ run_id: entry.run_id,
188
+ spec_number: entry.spec,
189
+ demo: entry.demo,
190
+ started_at: entry.started_at,
191
+ log_path: entry.log_path,
192
+ }),
193
+ next_seq: 1,
194
+ append_failures: 0,
195
+ rows_stopped: false,
196
+ unsent_rows: new Map(),
197
+ }
198
+ const { rows } = applyInOrder({ memory, records })
199
+ // A run whose engine ended stays ended after a plugin restart.
200
+ if (entry.ended) {
201
+ memory.state = applyEnded({
202
+ state: memory.state,
203
+ ended: entry.ended,
204
+ })
205
+ } else {
206
+ memory.unsent_rows = rows
207
+ }
208
+ runs.set(entry.run_id, memory)
209
+ return memory
210
+ }
211
+
212
+ /** A run's journal records on disk; none without a runs folder. */
213
+ const journalOf = ({ run_id }: { run_id: string }) =>
214
+ runs_dir === null
215
+ ? Promise.resolve([])
216
+ : readJournal({ runs_dir, run_id, log })
217
+
218
+ /** Rebuilds every run in the registry from its journal. */
219
+ const load = async () => {
220
+ const entries = registry.list()
221
+ const journals = await Promise.all(
222
+ entries.map(({ run_id }) => journalOf({ run_id }))
223
+ )
224
+ entries.forEach((entry, index) =>
225
+ remember({ entry, records: journals[index] ?? [] })
226
+ )
227
+ }
228
+
229
+ /** Changes a run's registry entry, and its copy in memory. */
230
+ const updateEntry = ({
231
+ memory,
232
+ change,
233
+ }: {
234
+ memory: RunMemory
235
+ change: Partial<Pick<RunEntry, 'ended' | 'restarts' | 'token'>>
236
+ }) => {
237
+ memory.entry = registry.update({
238
+ run_id: memory.entry.run_id,
239
+ change,
240
+ }) ?? { ...memory.entry, ...change }
241
+ }
242
+
243
+ /** Marks a run's engine as ended (kept in the registry) and updates its header. */
244
+ const endRun = async ({
245
+ memory,
246
+ ended,
247
+ }: {
248
+ memory: RunMemory
249
+ ended: EngineEnded
250
+ }) => {
251
+ memory.state = applyEnded({ state: memory.state, ended })
252
+ memory.unsent_rows.clear()
253
+ updateEntry({ memory, change: { ended } })
254
+ await appendRows({ memory, rows: [header({ memory })] })
255
+ }
256
+
257
+ // Every method waits for the registry's runs to be rebuilt first.
258
+ const ready = load().catch((error: unknown) => {
259
+ log(
260
+ `Couldn't rebuild the runs from their journals: ${errorText({ error })}`
261
+ )
262
+ })
263
+
264
+ return {
265
+ registry,
266
+ runs,
267
+ queues,
268
+ poster,
269
+ enqueue,
270
+ appendRows,
271
+ header,
272
+ applyInOrder,
273
+ remember,
274
+ journalOf,
275
+ updateEntry,
276
+ endRun,
277
+ ready,
278
+ }
279
+ }
280
+
281
+ export type RunStore = ReturnType<typeof createRunStore>
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The board server's types: what it spawns and runs, what it is given, and
3
+ * what it keeps in memory for each run.
4
+ */
5
+ import type { RunEntry } from './run-registry'
6
+
7
+ import type { BoardRow } from '../shared/board-rows'
8
+ import type { BoardState } from '../shared/board-state'
9
+ import type { EngineSettings } from '../shared/engine-settings'
10
+
11
+ /** What the server asks the host to spawn: detached, output to a log file. */
12
+ export type SpawnRequest = {
13
+ command: string
14
+ args: string[]
15
+ env: Record<string, string>
16
+ cwd: string
17
+ log_path: string
18
+ /** Called if the process fails to start after `spawn_engine` returned. */
19
+ on_error: (error: unknown) => void
20
+ }
21
+
22
+ export type SpawnEngine = (request: SpawnRequest) => { pid: number | null }
23
+
24
+ /** A short command the server runs and waits for, such as `luca-run --unfinished`. */
25
+ export type CommandRequest = {
26
+ command: string
27
+ args: string[]
28
+ cwd: string
29
+ env: Record<string, string>
30
+ timeout_ms: number
31
+ }
32
+
33
+ /** How a command ended: `exit_code` is `null` when it was killed or timed out. */
34
+ export type CommandResult = {
35
+ exit_code: number | null
36
+ stdout: string
37
+ stderr: string
38
+ }
39
+
40
+ /** Runs a command to its end. Never throws: a failure is in the result. */
41
+ export type RunCommand = (request: CommandRequest) => Promise<CommandResult>
42
+
43
+ /** Every running process's full command line. */
44
+ export type ListProcesses = () => Promise<string[]>
45
+
46
+ /** What one engine check did, by run id. */
47
+ export type EngineCheck = { restarted: string[]; stopped: string[] }
48
+
49
+ /** Appends (or replaces, by id) one row in a chat's timeline. */
50
+ export type AppendRow = ({
51
+ agent_id,
52
+ row,
53
+ }: {
54
+ agent_id: string
55
+ row: BoardRow
56
+ }) => Promise<void>
57
+
58
+ /** A run's board, built from its journal records so far. */
59
+ export type Replay = {
60
+ state: BoardState
61
+ /** The next journal seq the board wants (starts at 1). */
62
+ next_seq: number
63
+ }
64
+
65
+ /** One run the plugin started: its registry entry and its board in memory. */
66
+ export type RunMemory = Replay & {
67
+ entry: RunEntry
68
+ append_failures: number
69
+ rows_stopped: boolean
70
+ /**
71
+ * The rows of records rebuilt from disk, by seq, that the chat may not
72
+ * have: the engine may have written them while the plugin was down.
73
+ * Added when the engine sends those records again.
74
+ */
75
+ unsent_rows: Map<number, BoardRow[]>
76
+ }
77
+
78
+ /**
79
+ * A run the plugin didn't start (such as one from the command line), read
80
+ * from its journal on disk. Read-only: no chat rows, no engine events, no
81
+ * restarts. `size` and `changed_ms` are its journal's when last read.
82
+ * `repo` is the repo its `run_started` names, `null` when it names none.
83
+ * `engine_gone` is set by the engine check (#635): no process names the
84
+ * run and its live file isn't fresh. It only labels the run.
85
+ */
86
+ export type OutsideRun = Replay & {
87
+ size: number
88
+ changed_ms: number
89
+ repo: string | null
90
+ engine_gone: boolean
91
+ }
92
+
93
+ /**
94
+ * What `createBoardServer` is given. Every side effect is injected, so tests
95
+ * drive the server without Paseo.
96
+ */
97
+ export type BoardServerConfig = {
98
+ registry_path: string
99
+ /** The engine's runs folder (`<runs_dir>/<run_id>/journal.jsonl`). */
100
+ runs_dir?: string | null
101
+ append_row: AppendRow
102
+ spawn_engine: SpawnEngine
103
+ list_processes: ListProcesses
104
+ run_command: RunCommand
105
+ read_settings: () => Promise<EngineSettings>
106
+ file_exists: ({ path }: { path: string }) => boolean
107
+ /** Follows an installed `luca-run`'s symlink; the path as is without it. */
108
+ real_path?: ({ path }: { path: string }) => string
109
+ home_dir: string
110
+ env: Record<string, string | undefined>
111
+ log_dir: string
112
+ now: () => Date
113
+ log: (message: string) => void
114
+ }
@@ -0,0 +1,4 @@
1
+ /** Small things the board server's modules share. */
2
+
3
+ export const errorText = ({ error }: { error: unknown }): string =>
4
+ error instanceof Error ? error.message : String(error)