@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -1,15 +1,13 @@
1
- import { readdir } from 'node:fs/promises'
2
- import { join } from 'node:path'
3
-
4
- import { setUpBoard } from './board-in-paseo'
5
1
  import type { PaseoPlugins } from './computer-adapters'
6
- import {
7
- formatChecks,
8
- hasProblem,
9
- reason,
10
- type DoctorCheck,
11
- } from './doctor-checks'
2
+ import { reason, type DoctorCheck } from './doctor-checks'
12
3
  import { describeRun, goingRuns } from './going-runs'
4
+ import type { ListProcesses } from './live-runs'
5
+ import { resumeCommand } from './run-modes'
6
+ import {
7
+ boardFingerprint,
8
+ finishUpgrade,
9
+ type UpgradeEnd,
10
+ } from './upgrade-finish'
13
11
 
14
12
  import { LUCA_PACKAGE } from '../config/luca-version'
15
13
 
@@ -18,7 +16,11 @@ import { LUCA_PACKAGE } from '../config/luca-version'
18
16
  * never under a going run.
19
17
  *
20
18
  * 1. Refuses while any run is going (limit waits and stuck runs included)
21
- * and lists them by spec and repo.
19
+ * and lists them by spec and repo. A run is going when `ps` shows its
20
+ * engine, or its engine is gone but the board will restart it; when `ps`
21
+ * fails, every unfinished run is (see `goingRuns`). An unfinished run
22
+ * nothing runs (such as a crash) doesn't stop it: upgrade says how to
23
+ * resume it on the new version.
22
24
  * 2. Picks the version: `to` exactly when given, older ones included (the
23
25
  * way back from a broken version). Without it, the installed version's
24
26
  * channel: an alpha install gets the `alpha` tag, any other gets
@@ -28,10 +30,20 @@ import { LUCA_PACKAGE } from '../config/luca-version'
28
30
  * 4. Reloads the board in Paseo, keeping its settings, and rewrites its
29
31
  * engine and Bun paths (see `setUpBoard`).
30
32
  * 5. Says when `/reload-skills` is needed: when the board's files changed.
31
- * 6. Ends with doctor's computer checks (see `computerChecks`).
33
+ * 6. Copies Luca's own skills (every one in the new install's `skills/`)
34
+ * into `~/.claude/skills` (see `installLucaSkills`).
35
+ * 7. Ends with doctor's computer checks (see `computerChecks`).
36
+ *
37
+ * Steps 4-7 (`finishUpgrade`) run in the new install, not in this process
38
+ * (#529): this process runs the old version's code, which doesn't know
39
+ * what the new version's board setup, skills, and checks are. So after
40
+ * step 3 upgrade hands off to the new install's `luca upgrade --finish`,
41
+ * passes its exit code through, and doesn't run them itself. A new install
42
+ * that can't finish (a version from before #529, such as a downgrade's)
43
+ * gets them from this process, as before.
32
44
  *
33
- * npm's registry, Bun, Paseo, and the computer checks are adapters, so
34
- * tests use fakes.
45
+ * npm's registry, Bun, Paseo, `ps`, the new install, and the computer
46
+ * checks are adapters, so tests use fakes.
35
47
  */
36
48
 
37
49
  /** npm's registry lookups for `@alecsibilia/luca`. */
@@ -46,8 +58,28 @@ export type BunGlobal = {
46
58
  addGlobal: (args: { spec: string }) => Promise<void>
47
59
  }
48
60
 
49
- /** How upgrade ended; `message` is the last line it printed. */
50
- export type UpgradeEnd = { ok: boolean; message: string }
61
+ export type { UpgradeEnd }
62
+
63
+ /** How handing the last steps to the new install went. */
64
+ export type HandOff =
65
+ /** The new install ran them; it exited with `exit_code`. */
66
+ | { handed_off: true; exit_code: number }
67
+ /** The new install can't run them, and `why` (one sentence, no period). */
68
+ | { handed_off: false; why: string }
69
+
70
+ /** The newly installed Luca. */
71
+ export type NewInstall = {
72
+ /**
73
+ * Runs `luca upgrade --finish` in the new install of `version`, which
74
+ * prints its own lines; or says why it can't. Throws when the new
75
+ * install couldn't be started.
76
+ */
77
+ finish: (args: {
78
+ version: string
79
+ /** The board folder's fingerprint before the install. */
80
+ board_before: string | null
81
+ }) => Promise<HandOff>
82
+ }
51
83
 
52
84
  /** The first version of new Luca; without `--to`, nothing older is installed. */
53
85
  const FIRST_NEW_MAJOR = 14
@@ -115,74 +147,61 @@ export const channelTarget = ({
115
147
  return { ok: true, version }
116
148
  }
117
149
 
118
- /**
119
- * A fingerprint of every file in `dir` but its `node_modules`, or `null`
120
- * when it can't be read.
121
- */
122
- const fingerprint = async (dir: string): Promise<string | null> => {
123
- const lines: string[] = []
124
- const walk = async (at: string): Promise<void> => {
125
- const entries = await readdir(at, { withFileTypes: true })
126
- for (const entry of entries) {
127
- const path = join(at, entry.name)
128
- if (entry.isDirectory()) {
129
- if (entry.name !== 'node_modules') await walk(path)
130
- } else if (entry.isFile()) {
131
- const bytes = await Bun.file(path).arrayBuffer()
132
- lines.push(`${path}\t${Bun.hash(bytes)}`)
133
- }
134
- }
135
- }
136
- try {
137
- await walk(dir)
138
- } catch {
139
- return null
140
- }
141
- return lines.sort().join('\n')
142
- }
143
-
144
150
  /**
145
151
  * Runs `luca upgrade`: refuses while a run is going, picks the version,
146
- * installs it with Bun, reloads the board with its settings kept and its
147
- * paths rewritten, then prints `computer_checks`. Never throws: a refusal,
148
- * a failed step, or a check that is a problem ends it with `ok: false`.
152
+ * installs it with Bun, then hands the last steps to `new_install` (see
153
+ * `finishUpgrade`), or runs them here when it can't take them. Never
154
+ * throws: a refusal, a failed step, or a check that is a problem ends it
155
+ * with `ok: false`.
149
156
  *
150
157
  * @example
151
- * const end = await runUpgrade({ to: null, installed_version: lucaVersion(), runs_dir: defaultRunsDir(), registry_path, npm, bun, paseo, board_dir, engine_path, bun_path, computer_checks, log: console.log })
158
+ * const end = await runUpgrade({ to: null, home: homedir(), installed_version: lucaVersion(), runs_dir: defaultRunsDir(), registry_path, list_processes: listProcesses, npm, bun, paseo, board_dir, skills_dir, engine_path, bun_path, new_install, computer_checks, log: console.log })
152
159
  */
153
160
  export const runUpgrade = async ({
154
161
  to,
162
+ home,
155
163
  installed_version,
156
164
  runs_dir,
157
165
  registry_path,
166
+ list_processes,
158
167
  npm,
159
168
  bun,
160
169
  paseo,
161
170
  board_dir,
171
+ skills_dir,
162
172
  engine_path,
163
173
  bun_path,
174
+ new_install,
164
175
  computer_checks,
165
176
  log,
166
177
  }: {
167
178
  /** `--to <version>`, or `null` to stay on the installed channel. */
168
179
  to: string | null
180
+ /** The home folder, whose `.claude/skills` gets Luca's own skills. */
181
+ home: string
169
182
  /** The version of Luca installed now. */
170
183
  installed_version: string
171
184
  runs_dir: string
172
185
  /** The board's run registry. */
173
186
  registry_path: string
187
+ /** Every running process's command line, to tell live engines from gone ones. */
188
+ list_processes: ListProcesses
174
189
  npm: NpmRegistry
175
190
  bun: BunGlobal
176
191
  paseo: PaseoPlugins
177
192
  /** The board folder inside Luca's install folder. */
178
193
  board_dir: string
194
+ /** The skills folder inside Luca's install folder. */
195
+ skills_dir: string
179
196
  /** The real path of the installed `luca-run`. */
180
197
  engine_path: string
181
198
  /** Bun's own path. */
182
199
  bun_path: string
200
+ /** The install `bun add -g` makes, which finishes the upgrade. */
201
+ new_install: NewInstall
183
202
  /**
184
203
  * Doctor's computer checks of the new install, run after the board
185
- * reload; none when not given.
204
+ * reload when this process runs the last steps; none when not given.
186
205
  */
187
206
  computer_checks?: () => Promise<DoctorCheck[]>
188
207
  log: (line: string) => void
@@ -192,17 +211,30 @@ export const runUpgrade = async ({
192
211
  return { ok, message }
193
212
  }
194
213
  try {
195
- const going = await goingRuns({ runs_dir, registry_path })
214
+ const going = await goingRuns({
215
+ runs_dir,
216
+ registry_path,
217
+ list_processes,
218
+ })
196
219
  if (!going.ok) {
197
220
  return say({ message: `Refused: ${going.error}`, ok: false })
198
221
  }
199
222
  if (going.runs.length > 0) {
223
+ const unchecked =
224
+ going.ps_error === null
225
+ ? ''
226
+ : ` Luca couldn't list the processes (${going.ps_error}), so every unfinished run counts as going.`
200
227
  return say({
201
- message: `Refused: runs are going, so an upgrade would change their engine halfway. Wait for them to end (or stop them), then try again:\n${going.runs.map(describeRun).join('\n')}`,
228
+ message: `Refused: runs are going, so an upgrade would change their engine halfway.${unchecked} Wait for them to end (or stop them), then try again:\n${going.runs.map(describeRun).join('\n')}`,
202
229
  ok: false,
203
230
  })
204
231
  }
205
232
  log('[luca upgrade] no run is going')
233
+ if (going.stopped.length > 0) {
234
+ log(
235
+ `[luca upgrade] These runs stopped before they ended, and nothing runs them now. They don't stop the upgrade. To go on with one on the new version, after the upgrade:\n${going.stopped.map((run) => `${describeRun(run)}: ${resumeCommand(run)}`).join('\n')}`
236
+ )
237
+ }
206
238
 
207
239
  let version: string
208
240
  if (to !== null) {
@@ -222,39 +254,48 @@ export const runUpgrade = async ({
222
254
  })
223
255
  }
224
256
 
225
- const before = await fingerprint(board_dir)
257
+ const before = await boardFingerprint(board_dir)
226
258
  const spec = `${LUCA_PACKAGE}@${version}`
227
259
  log(`[luca upgrade] installing ${spec} (from ${installed_version})`)
228
260
  await bun.addGlobal({ spec })
229
261
  log(`[luca upgrade] installed ${spec}`)
230
262
 
231
- const board = await setUpBoard({
232
- command: 'luca upgrade',
263
+ let hand_off: HandOff
264
+ try {
265
+ hand_off = await new_install.finish({
266
+ version,
267
+ board_before: before,
268
+ })
269
+ } catch (error) {
270
+ return say({
271
+ message: `Luca ${version} is installed, but Luca couldn't start it to finish the upgrade (${reason(error)}). Run luca init to finish: it reloads the board and copies Luca's skills.`,
272
+ ok: false,
273
+ })
274
+ }
275
+ if (hand_off.handed_off) {
276
+ // The new install printed its own lines, its last one included.
277
+ return hand_off.exit_code === 0
278
+ ? { ok: true, message: `Luca ${version} is installed.` }
279
+ : say({
280
+ message: `Luca ${version} is installed, but its last steps failed (exit ${hand_off.exit_code}). See above for what to fix.`,
281
+ ok: false,
282
+ })
283
+ }
284
+ log(
285
+ `[luca upgrade] ${hand_off.why}, so Luca ${installed_version} does the last steps.`
286
+ )
287
+ return await finishUpgrade({
288
+ version,
289
+ board_before: before,
290
+ home,
233
291
  paseo,
234
- // Upgrade only reloads; `luca init` asks to turn plugins on.
235
- ask: async () => false,
236
292
  board_dir,
293
+ skills_dir,
237
294
  engine_path,
238
295
  bun_path,
296
+ computer_checks,
239
297
  log,
240
298
  })
241
- if (board.ok) {
242
- const after = await fingerprint(board_dir)
243
- if (before === null || after === null || before !== after) {
244
- log(
245
- "[luca upgrade] The board's files changed: run /reload-skills in a Paseo chat so its slash commands are up to date."
246
- )
247
- }
248
- }
249
- const end = say({
250
- message: board.ok
251
- ? `Luca ${version} is installed.`
252
- : `Luca ${version} is installed, but the board wasn't reloaded. Run luca init to fix it.`,
253
- ok: board.ok,
254
- })
255
- const checks = (await computer_checks?.()) ?? []
256
- for (const line of formatChecks({ checks })) log(line)
257
- return { ...end, ok: end.ok && !hasProblem({ checks }) }
258
299
  } catch (error) {
259
300
  // Such as a failed `bun add -g` or registry lookup.
260
301
  return say({ message: reason(error), ok: false })
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Reads of single config fields, each with the default the engine uses when
3
+ * the config leaves it out: the prepare and test time limits, how many
4
+ * prepares run at once, the decision model, the models block, and the feature
5
+ * map's path.
6
+ */
7
+ import { normalize } from 'node:path'
8
+
9
+ import {
10
+ DecisionModelConfigSchema,
11
+ ModelsConfigSchema,
12
+ type EngineConfig,
13
+ type ModelsConfig,
14
+ } from './engine-config-schema'
15
+
16
+ import type { DecisionModelId } from '../jev/jev-schemas'
17
+ import { DEFAULT_COMMAND_TIMEOUT_MS } from '../shell/run-command'
18
+
19
+ /**
20
+ * How long the prepare command may run when the config doesn't say: 30
21
+ * minutes. A first build in a fresh worktree can take many minutes
22
+ * (HeartGold's takes about 8), far past the 5 minutes other commands get.
23
+ */
24
+ export const DEFAULT_PREPARE_TIMEOUT_MS = 30 * 60 * 1000
25
+
26
+ /**
27
+ * The prepare command's time limit: the config's `prepare_timeout_ms`, else
28
+ * the default.
29
+ *
30
+ * @example
31
+ * prepareTimeoutOf({ config: { ...config, prepare_timeout_ms: 3_600_000 } }) // 3_600_000
32
+ */
33
+ export const prepareTimeoutOf = ({
34
+ config,
35
+ }: {
36
+ config: EngineConfig
37
+ }): number => config.prepare_timeout_ms ?? DEFAULT_PREPARE_TIMEOUT_MS
38
+
39
+ /**
40
+ * Each test command's time limit (#542): the config's `test_timeout_ms`,
41
+ * else the engine's default for any command. It holds for every run of a
42
+ * test command (baseline, red check, gates); types and lint keep the default.
43
+ *
44
+ * @example
45
+ * testTimeoutOf({ config: { ...config, test_timeout_ms: 900_000 } }) // 900_000
46
+ */
47
+ export const testTimeoutOf = ({ config }: { config: EngineConfig }): number =>
48
+ config.test_timeout_ms ?? DEFAULT_COMMAND_TIMEOUT_MS
49
+
50
+ /**
51
+ * How many prepare runs may go at once when the config doesn't say: 1. A
52
+ * build is often heavy on the CPU, so builds at the same time only slow each
53
+ * other down (HeartGold's four first builds took 19 to 25 minutes each at
54
+ * once, against 8 to 10 alone).
55
+ */
56
+ export const DEFAULT_PREPARE_CONCURRENCY = 1
57
+
58
+ /**
59
+ * How many prepare runs may go at once: the config's `prepare_concurrency`,
60
+ * else the default.
61
+ *
62
+ * @example
63
+ * prepareConcurrencyOf({ config: { ...config, prepare_concurrency: 2 } }) // 2
64
+ */
65
+ export const prepareConcurrencyOf = ({
66
+ config,
67
+ }: {
68
+ config: EngineConfig
69
+ }): number => config.prepare_concurrency ?? DEFAULT_PREPARE_CONCURRENCY
70
+
71
+ /**
72
+ * The decision model a run asks: the config's `decision_model.model`, else
73
+ * Clef (`DEFAULT_DECISION_MODEL`).
74
+ *
75
+ * @example
76
+ * decisionModelOf({ config: { ...config, decision_model: { model: '@cf/cloudflare/clef-flash' } } })
77
+ * // '@cf/cloudflare/clef-flash'
78
+ */
79
+ export const decisionModelOf = ({
80
+ config,
81
+ }: {
82
+ config: EngineConfig
83
+ }): DecisionModelId =>
84
+ config.decision_model?.model ?? DecisionModelConfigSchema.parse({}).model
85
+
86
+ /**
87
+ * The run's model floors and confidence bar: the config's `models`, else
88
+ * the schema's defaults (every floor `opus`).
89
+ *
90
+ * @example
91
+ * modelsOf({ config }).floors['test-writer'] // 'opus'
92
+ */
93
+ export const modelsOf = ({ config }: { config: EngineConfig }): ModelsConfig =>
94
+ config.models ?? ModelsConfigSchema.parse({})
95
+
96
+ /**
97
+ * The feature map's path (#583), in the form a worktree-relative path takes
98
+ * everywhere else (`normalize`, no leading `./`), or `null` with no map.
99
+ * `./docs/feature-map.md` and `docs//feature-map.md` are both
100
+ * `docs/feature-map.md`. The role rules, the permission rules, the prompts,
101
+ * and the red check all read the map's path here, so they agree on it
102
+ * whatever way the config spells it.
103
+ *
104
+ * @example
105
+ * featureMapPath({ config: { ...config, feature_map: './docs/feature-map.md' } }) // 'docs/feature-map.md'
106
+ */
107
+ export const featureMapPath = ({
108
+ config,
109
+ }: {
110
+ config: EngineConfig
111
+ }): string | null =>
112
+ config.feature_map === undefined
113
+ ? null
114
+ : normalize(config.feature_map).replace(/^\.\//, '')
115
+
116
+ /**
117
+ * Whether a feature map path names a folder of pages (#597): any path not
118
+ * ending in `.md`. The path alone decides, so the guard and the prompts,
119
+ * which never read the disk, agree with the red check. `loadEngineConfig`
120
+ * refuses such a path when it is a file, so a map file must end in `.md`.
121
+ *
122
+ * @example
123
+ * isFeatureMapFolder({ path: 'docs/feature-map' }) // true
124
+ * isFeatureMapFolder({ path: 'docs/feature-map.md' }) // false
125
+ */
126
+ export const isFeatureMapFolder = ({ path }: { path: string }): boolean =>
127
+ !path.toLowerCase().endsWith('.md')
@@ -0,0 +1,250 @@
1
+ import isEqual from 'lodash/isEqual'
2
+ import omit from 'lodash/omit'
3
+ import reduce from 'lodash/reduce'
4
+ import { z } from 'zod'
5
+
6
+ import type { EngineConfig } from './engine-config'
7
+
8
+ /**
9
+ * The config fields a resumed run takes from the repo's
10
+ * `.luca/config.json` again (#516). They say how the engine builds a
11
+ * checkout before its tests, how long a test may run, or which new docs
12
+ * may be committed, not what the tests are, so a run can change them
13
+ * halfway without making its earlier results wrong: a repo that swaps a
14
+ * slow `prepare` for a fast cached one gets it on the next resume.
15
+ *
16
+ * - `prepare`: the command run before each test run in a checkout.
17
+ * - `prepare_timeout_ms`: how long that command may run.
18
+ * - `prepare_concurrency`: how many prepare runs may go at once. The
19
+ * prepare slots take the limit on each call (`prepareCheck`), so a new
20
+ * one counts from the next prepare run.
21
+ * - `test_concurrency`: how many test runs may go at once (#544). The test
22
+ * slots take the limit on each call (`inTestSlot`), as the prepare slots
23
+ * do, so a new one counts from the next test run.
24
+ * - `test_timeout_ms`: how long each test command may run (#542). Each run
25
+ * of a test command reads it then, so a new one counts from the next.
26
+ * - `test_case_timeout_ms`: how long one fast test may take (#580). Each run
27
+ * of a fast test command reads it then, so a new one counts from the next.
28
+ * - `leftover_allow`: more new markdown the leftover scan lets through
29
+ * (#520). Each scan reads it then, so a run stuck on a leftover can be
30
+ * unstuck by allowing the file and resuming.
31
+ */
32
+ export const RELOADED_FIELDS = [
33
+ 'prepare',
34
+ 'prepare_timeout_ms',
35
+ 'prepare_concurrency',
36
+ 'test_concurrency',
37
+ 'test_timeout_ms',
38
+ 'test_case_timeout_ms',
39
+ 'leftover_allow',
40
+ ] as const
41
+
42
+ /**
43
+ * The config fields a run keeps from its `run_started`, whatever the file
44
+ * says on a resume:
45
+ *
46
+ * - `checks`: the baseline, the red check, and the gates compare test
47
+ * results across the run. A ticket takes another's baseline by commit
48
+ * alone (`sharedBaselineStep` in `core/decide-build.ts`), and the red
49
+ * check holds every old passing test of the baseline to the new run by
50
+ * file and name (`checkRed` in `gates/red-check.ts`). A baseline taken
51
+ * with old commands is not comparable with a run of new ones. Intake also
52
+ * judged the run's tickets against these commands (a `bun` test command
53
+ * for the red check).
54
+ * - `test_file_patterns`: which files are tests decides the red check, the
55
+ * role rules, and which commits were the test-writer's.
56
+ * - `test_helper_patterns`: which non-test files the test-writer may write
57
+ * (#619), as `test_file_patterns` says which test files it may.
58
+ * - `test_setup_files`: part of what the tests are, as `checks` is.
59
+ * - `feature_map`: says which test files belong to which feature (#583), as
60
+ * `test_file_patterns` says which files are tests: the red check holds a
61
+ * ticket's new test files to it, and the role rules let the test-writer
62
+ * edit it.
63
+ * - `rule_files`: the rules lens judges against them; tickets already
64
+ * reviewed were judged on the old ones.
65
+ * - `muninn`: the run's memory vault is fixed at start (`run_started.memory`).
66
+ * - `run_budget_tokens`: the budget the run's tokens are counted against,
67
+ * and the board's, from `run_started`; a raise is a `retry` reply.
68
+ * - `decision_model`: the model shadow mode asks (#534), so a run's asks
69
+ * are all of one model and can be scored together.
70
+ * - `models`: the model floors and the confidence bar (#603), so one run's
71
+ * routing stays consistent.
72
+ * - `generators`: the commands writers may ask the engine to run (#618),
73
+ * whose output is committed with the tickets, as the prompts named them.
74
+ */
75
+ export const FROZEN_FIELDS = [
76
+ 'checks',
77
+ 'test_file_patterns',
78
+ 'test_helper_patterns',
79
+ 'test_setup_files',
80
+ 'feature_map',
81
+ 'rule_files',
82
+ 'muninn',
83
+ 'run_budget_tokens',
84
+ 'decision_model',
85
+ 'models',
86
+ 'generators',
87
+ ] as const
88
+
89
+ const positiveInt = z.number().int().positive().nullable()
90
+ const patterns = z.array(z.string().min(1)).nullable()
91
+
92
+ /**
93
+ * One reloaded field that changed: its value in the run before (`from`)
94
+ * and in the file now (`to`). `null` is a field left out, so a field added
95
+ * or removed is a change too.
96
+ */
97
+ export const ConfigChangeSchema = z.discriminatedUnion('field', [
98
+ z.object({
99
+ field: z.literal('prepare'),
100
+ from: z.string().min(1).nullable(),
101
+ to: z.string().min(1).nullable(),
102
+ }),
103
+ z.object({
104
+ field: z.literal('prepare_timeout_ms'),
105
+ from: positiveInt,
106
+ to: positiveInt,
107
+ }),
108
+ z.object({
109
+ field: z.literal('prepare_concurrency'),
110
+ from: positiveInt,
111
+ to: positiveInt,
112
+ }),
113
+ z.object({
114
+ field: z.literal('test_concurrency'),
115
+ from: positiveInt,
116
+ to: positiveInt,
117
+ }),
118
+ z.object({
119
+ field: z.literal('test_timeout_ms'),
120
+ from: positiveInt,
121
+ to: positiveInt,
122
+ }),
123
+ z.object({
124
+ field: z.literal('test_case_timeout_ms'),
125
+ from: positiveInt,
126
+ to: positiveInt,
127
+ }),
128
+ z.object({
129
+ field: z.literal('leftover_allow'),
130
+ from: patterns,
131
+ to: patterns,
132
+ }),
133
+ ])
134
+
135
+ export type ConfigChange = z.infer<typeof ConfigChangeSchema>
136
+
137
+ /**
138
+ * The reloaded fields whose value differs between the run's config
139
+ * (`current`) and the one just read (`next`), old to new, in
140
+ * `RELOADED_FIELDS` order. Frozen fields are never changes. Pure.
141
+ *
142
+ * @example
143
+ * configChanges({ current: { ...config, prepare: 'make' }, next: { ...config, prepare: 'make fast' } })
144
+ * // [{ field: 'prepare', from: 'make', to: 'make fast' }]
145
+ */
146
+ export const configChanges = ({
147
+ current,
148
+ next,
149
+ }: {
150
+ current: EngineConfig
151
+ next: EngineConfig
152
+ }): ConfigChange[] => {
153
+ const all: ConfigChange[] = [
154
+ {
155
+ field: 'prepare',
156
+ from: current.prepare ?? null,
157
+ to: next.prepare ?? null,
158
+ },
159
+ {
160
+ field: 'prepare_timeout_ms',
161
+ from: current.prepare_timeout_ms ?? null,
162
+ to: next.prepare_timeout_ms ?? null,
163
+ },
164
+ {
165
+ field: 'prepare_concurrency',
166
+ from: current.prepare_concurrency ?? null,
167
+ to: next.prepare_concurrency ?? null,
168
+ },
169
+ {
170
+ field: 'test_concurrency',
171
+ from: current.test_concurrency ?? null,
172
+ to: next.test_concurrency ?? null,
173
+ },
174
+ {
175
+ field: 'test_timeout_ms',
176
+ from: current.test_timeout_ms ?? null,
177
+ to: next.test_timeout_ms ?? null,
178
+ },
179
+ {
180
+ field: 'test_case_timeout_ms',
181
+ from: current.test_case_timeout_ms ?? null,
182
+ to: next.test_case_timeout_ms ?? null,
183
+ },
184
+ {
185
+ field: 'leftover_allow',
186
+ from: current.leftover_allow ?? null,
187
+ to: next.leftover_allow ?? null,
188
+ },
189
+ ]
190
+ return all.filter(({ from, to }) => !isEqual(from, to))
191
+ }
192
+
193
+ const applyChange = ({
194
+ config,
195
+ change,
196
+ }: {
197
+ config: EngineConfig
198
+ change: ConfigChange
199
+ }): EngineConfig => {
200
+ switch (change.field) {
201
+ case 'prepare':
202
+ return change.to === null
203
+ ? omit(config, 'prepare')
204
+ : { ...config, prepare: change.to }
205
+ case 'prepare_timeout_ms':
206
+ return change.to === null
207
+ ? omit(config, 'prepare_timeout_ms')
208
+ : { ...config, prepare_timeout_ms: change.to }
209
+ case 'prepare_concurrency':
210
+ return change.to === null
211
+ ? omit(config, 'prepare_concurrency')
212
+ : { ...config, prepare_concurrency: change.to }
213
+ case 'test_concurrency':
214
+ return change.to === null
215
+ ? omit(config, 'test_concurrency')
216
+ : { ...config, test_concurrency: change.to }
217
+ case 'test_timeout_ms':
218
+ return change.to === null
219
+ ? omit(config, 'test_timeout_ms')
220
+ : { ...config, test_timeout_ms: change.to }
221
+ case 'test_case_timeout_ms':
222
+ return change.to === null
223
+ ? omit(config, 'test_case_timeout_ms')
224
+ : { ...config, test_case_timeout_ms: change.to }
225
+ case 'leftover_allow':
226
+ return change.to === null
227
+ ? omit(config, 'leftover_allow')
228
+ : { ...config, leftover_allow: change.to }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * `config` with each change's new value: set, or left out for `null`. Pure.
234
+ *
235
+ * @example
236
+ * withConfigChanges({ config, changes: [{ field: 'prepare', from: 'make', to: null }] })
237
+ * // config without prepare
238
+ */
239
+ export const withConfigChanges = ({
240
+ config,
241
+ changes,
242
+ }: {
243
+ config: EngineConfig
244
+ changes: ConfigChange[]
245
+ }): EngineConfig =>
246
+ reduce(
247
+ changes,
248
+ (next, change) => applyChange({ config: next, change }),
249
+ config
250
+ )