@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,122 @@
1
+ /**
2
+ * The runs in the runs folder that are not over, and whether Paseo may
3
+ * restart each one (#375): `unfinishedRuns`, `restartableRuns`, and the shape
4
+ * `luca-run --unfinished` prints.
5
+ */
6
+ import { existsSync, readdirSync } from 'node:fs'
7
+
8
+ import { z } from 'zod'
9
+
10
+ import { readRun } from './read-run'
11
+
12
+ import { decide } from '../core/decide'
13
+ import { STOP_ACTIONS } from '../core/execute'
14
+ import type { JournalRecord } from '../journal/journal-record'
15
+
16
+ /**
17
+ * The ids of the runs in `runs_dir` that are not over: runs whose journal's
18
+ * next action (by the decision step) is not a stop (`done` or
19
+ * `invalid_journal`), such as a run that crashed, was killed, or waits for a
20
+ * reply. Folders with no journal, an empty one, or one that cannot be read
21
+ * are left out. Sorted, so oldest first for made-up run ids. Paseo can
22
+ * `resumeRun` each one when it starts (#375).
23
+ *
24
+ * @example
25
+ * for (const run_id of unfinishedRuns({ runs_dir: defaultRunsDir() })) console.log(run_id)
26
+ */
27
+ export const unfinishedRuns = ({
28
+ runs_dir,
29
+ }: {
30
+ runs_dir: string
31
+ }): string[] => {
32
+ if (!existsSync(runs_dir)) return []
33
+ return readdirSync(runs_dir, { withFileTypes: true })
34
+ .filter((entry) => entry.isDirectory())
35
+ .map(({ name }) => name)
36
+ .filter((run_id) => {
37
+ const read = readRun({ runs_dir, run_id })
38
+ return (
39
+ read.ok &&
40
+ !STOP_ACTIONS.has(decide({ records: read.records }).type)
41
+ )
42
+ })
43
+ .toSorted()
44
+ }
45
+
46
+ /**
47
+ * One unfinished run, as `luca-run --unfinished` prints it for the board
48
+ * plugin (#375): whether Paseo may restart it with `--resume`, and why.
49
+ *
50
+ * - `resumable`: the engine was cut off (a crash, a kill); `restart` is true.
51
+ * - `launcher_stopped`: its last record is a launcher stop (the wrong login,
52
+ * plan, or model). A restart would stop the same way, so `restart` is
53
+ * false; `message` is the stop's reason.
54
+ * - `billing_stopped`: it stopped for billing, which sticks; `restart` is
55
+ * false and `message` is the stop's reason.
56
+ */
57
+ export const RestartableRunSchema = z.object({
58
+ run_id: z.string(),
59
+ restart: z.boolean(),
60
+ reason: z.enum(['resumable', 'launcher_stopped', 'billing_stopped']),
61
+ message: z.string().nullable(),
62
+ })
63
+
64
+ export type RestartableRun = z.infer<typeof RestartableRunSchema>
65
+
66
+ /** What `luca-run --unfinished` prints: one JSON object, snake_case keys. */
67
+ export const UnfinishedOutputSchema = z.object({
68
+ runs: z.array(RestartableRunSchema),
69
+ })
70
+
71
+ /** Whether Paseo may restart one unfinished run, from its records. */
72
+ const restartOf = ({
73
+ run_id,
74
+ records,
75
+ }: {
76
+ run_id: string
77
+ records: JournalRecord[]
78
+ }): RestartableRun => {
79
+ // A billing stop sticks, wherever it is in the journal.
80
+ const billing = records.find(
81
+ (record) => record.kind === 'run_stopped' && record.content.billing
82
+ )
83
+ if (billing?.kind === 'run_stopped') {
84
+ return {
85
+ run_id,
86
+ restart: false,
87
+ reason: 'billing_stopped',
88
+ message: billing.content.reason,
89
+ }
90
+ }
91
+ const last = records.at(-1)
92
+ if (last?.kind === 'run_stopped' && !last.content.crashed) {
93
+ return {
94
+ run_id,
95
+ restart: false,
96
+ reason: 'launcher_stopped',
97
+ message: last.content.reason,
98
+ }
99
+ }
100
+ return { run_id, restart: true, reason: 'resumable', message: null }
101
+ }
102
+
103
+ /**
104
+ * The unfinished runs in `runs_dir` (`unfinishedRuns`), each with whether
105
+ * Paseo may restart it with `--resume` (#375). A run whose last record is a
106
+ * launcher stop is not restarted, since it would stop the same way; a run
107
+ * with a billing stop anywhere in its journal never is. Everything else was
108
+ * cut off and can go on from its journal.
109
+ *
110
+ * @example
111
+ * const runs = restartableRuns({ runs_dir: defaultRunsDir() })
112
+ * // [{ run_id: 'luca-20260923-141500-ab12', restart: true, reason: 'resumable', message: null }]
113
+ */
114
+ export const restartableRuns = ({
115
+ runs_dir,
116
+ }: {
117
+ runs_dir: string
118
+ }): RestartableRun[] =>
119
+ unfinishedRuns({ runs_dir }).flatMap((run_id) => {
120
+ const read = readRun({ runs_dir, run_id })
121
+ return read.ok ? [restartOf({ run_id, records: read.records })] : []
122
+ })
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * `luca upgrade`: moves this computer to another published version of
3
- * Luca, never while a run is going:
3
+ * Luca, never while a run is going (its engine is running, or the board
4
+ * will restart it):
4
5
  *
5
6
  * luca upgrade [--to <version>]
6
7
  *
@@ -10,24 +11,45 @@
10
11
  * board in Paseo, keeping its settings, and ends with `luca doctor`'s
11
12
  * computer checks. See `runUpgrade`.
12
13
  *
14
+ * Those last steps run in the version just installed (#529): upgrade
15
+ * hands off to it, as
16
+ *
17
+ * luca upgrade --finish [--board-before <fingerprint>]
18
+ *
19
+ * which only Luca runs. It doesn't check for going runs or install
20
+ * anything: it reloads the board, copies Luca's skills, and runs the
21
+ * checks, with its own code. See `finishUpgrade`.
22
+ *
13
23
  * Exits 0 when done, 1 when it refused or a step failed, 2 on bad flags.
14
24
  */
25
+ import { realpath } from 'node:fs/promises'
15
26
  import { homedir } from 'node:os'
27
+ import { dirname, join } from 'node:path'
16
28
 
17
29
  import { z } from 'zod'
18
30
 
31
+ import type { Paseo } from './computer-adapters'
19
32
  import {
20
33
  claudeOf,
21
34
  computerOf,
22
35
  lucaInstall,
23
36
  muninnHealth,
24
37
  muninnOf,
38
+ packageOf,
25
39
  paseoOf,
26
40
  } from './computer-adapters-real'
27
41
  import { computerChecks } from './computer-checks'
28
42
  import { reason } from './doctor-checks'
29
43
  import { defaultRegistryPath } from './going-runs'
30
- import { runUpgrade, type BunGlobal, type NpmRegistry } from './upgrade'
44
+ import { listProcesses } from './live-runs'
45
+ import {
46
+ runUpgrade,
47
+ type BunGlobal,
48
+ type HandOff,
49
+ type NewInstall,
50
+ type NpmRegistry,
51
+ } from './upgrade'
52
+ import { finishUpgrade } from './upgrade-finish'
31
53
 
32
54
  import { LUCA_PACKAGE } from '../config/luca-version'
33
55
  import { defaultRunsDir } from '../journal/journal'
@@ -75,60 +97,220 @@ const bunOf = ({ home }: { home: string }): BunGlobal => ({
75
97
  },
76
98
  })
77
99
 
78
- /** The `--to` version, `null` without it, or `undefined` on bad flags. */
79
- const parseTo = (argv: string[]): string | null | undefined => {
80
- if (argv.length === 0) return null
81
- const [flag, version] = argv
82
- return flag === '--to' &&
83
- argv.length === 2 &&
84
- version !== undefined &&
85
- version !== '' &&
86
- !version.startsWith('-')
87
- ? version
88
- : undefined
100
+ /**
101
+ * The new install's file older Luca looks for (see `upgrade-finish.ts`):
102
+ * next to its `luca.ts` when it can run `luca upgrade --finish`.
103
+ */
104
+ const FINISH_MODULE = 'upgrade-finish.ts'
105
+
106
+ /**
107
+ * The new install's `luca` entry, when it is `version` of Luca and can
108
+ * finish an upgrade; otherwise why not, in one sentence without a period.
109
+ * Finds it the way the PATH does: the `luca` in Bun's global commands
110
+ * folder (`bun pm bin -g`), which `bun add -g` points at the new install.
111
+ * Read-only.
112
+ *
113
+ * @example
114
+ * await findNewInstall({ bin_dir: '/Users/me/.bun/bin', version: '14.0.0-alpha.11' })
115
+ * // { ok: true, entry: '/Users/me/.bun/install/global/node_modules/@alecsibilia/luca/engine/cli/luca.ts' }
116
+ */
117
+ export const findNewInstall = async ({
118
+ bin_dir,
119
+ version,
120
+ }: {
121
+ bin_dir: string
122
+ version: string
123
+ }): Promise<{ ok: true; entry: string } | { ok: false; why: string }> => {
124
+ const entry = await realpath(join(bin_dir, 'luca')).catch(() => null)
125
+ if (entry === null) {
126
+ return {
127
+ ok: false,
128
+ why: `Bun's global commands folder ${bin_dir} has no luca`,
129
+ }
130
+ }
131
+ const found = await packageOf(entry)
132
+ if (found?.name !== LUCA_PACKAGE || found.version !== version) {
133
+ const what =
134
+ found === null ? entry : `${found.name}@${found.version ?? '?'}`
135
+ return {
136
+ ok: false,
137
+ why: `The luca in ${bin_dir} is ${what}, not ${LUCA_PACKAGE}@${version}`,
138
+ }
139
+ }
140
+ if (!(await Bun.file(join(dirname(entry), FINISH_MODULE)).exists())) {
141
+ return {
142
+ ok: false,
143
+ why: `Luca ${version} can't finish an upgrade itself`,
144
+ }
145
+ }
146
+ return { ok: true, entry }
147
+ }
148
+
149
+ /**
150
+ * Runs `luca upgrade --finish` from `entry` with `bun_path`, its output
151
+ * going straight to this terminal; resolves to its exit code. Throws when
152
+ * it can't be started.
153
+ */
154
+ export const handOff = async ({
155
+ bun_path,
156
+ entry,
157
+ board_before,
158
+ cwd,
159
+ }: {
160
+ bun_path: string
161
+ entry: string
162
+ board_before: string | null
163
+ cwd: string
164
+ }): Promise<HandOff> => {
165
+ const child = Bun.spawn({
166
+ cmd: [
167
+ bun_path,
168
+ entry,
169
+ 'upgrade',
170
+ '--finish',
171
+ ...(board_before === null ? [] : ['--board-before', board_before]),
172
+ ],
173
+ cwd,
174
+ stdio: ['inherit', 'inherit', 'inherit'],
175
+ })
176
+ return { handed_off: true, exit_code: await child.exited }
177
+ }
178
+
179
+ /**
180
+ * The install `bun add -g` just made: found with `findNewInstall`, and
181
+ * handed the last steps with `handOff`. When it isn't found, or can't
182
+ * finish, says why, and this process runs them.
183
+ */
184
+ const newInstallOf = ({ home }: { home: string }): NewInstall => ({
185
+ finish: async ({ version, board_before }) => {
186
+ const bin = await runCommand({
187
+ cmd: [process.execPath, 'pm', 'bin', '-g'],
188
+ cwd: home,
189
+ timeout_ms: 30_000,
190
+ })
191
+ if (bin.exit_code !== 0) {
192
+ return {
193
+ handed_off: false,
194
+ why: `Bun didn't say where its global commands are (bun pm bin -g exited ${bin.exit_code})`,
195
+ }
196
+ }
197
+ const found = await findNewInstall({
198
+ bin_dir: bin.stdout.trim(),
199
+ version,
200
+ })
201
+ if (!found.ok) return { handed_off: false, why: found.why }
202
+ return handOff({
203
+ bun_path: process.execPath,
204
+ entry: found.entry,
205
+ board_before,
206
+ cwd: home,
207
+ })
208
+ },
209
+ })
210
+
211
+ /** What `luca upgrade`'s flags ask for. */
212
+ export type UpgradeArgs =
213
+ /** `luca upgrade [--to <version>]`; `to` is `null` without `--to`. */
214
+ | { finish: false; to: string | null }
215
+ /** `luca upgrade --finish [--board-before <fingerprint>]`, from an older Luca. */
216
+ | { finish: true; board_before: string | null }
217
+
218
+ /** A flag's value: not empty and not another flag. */
219
+ const isValue = (value: string | undefined): value is string =>
220
+ value !== undefined && value !== '' && !value.startsWith('-')
221
+
222
+ /**
223
+ * The flags after `upgrade`, or `undefined` when they're bad. Pure.
224
+ *
225
+ * @example
226
+ * parseUpgradeArgs({ argv: ['--to', '14.0.0-alpha.3'] }) // { finish: false, to: '14.0.0-alpha.3' }
227
+ * parseUpgradeArgs({ argv: ['--finish'] }) // { finish: true, board_before: null }
228
+ */
229
+ export const parseUpgradeArgs = ({
230
+ argv,
231
+ }: {
232
+ argv: string[]
233
+ }): UpgradeArgs | undefined => {
234
+ const [flag, value, ...rest] = argv
235
+ if (flag === undefined) return { finish: false, to: null }
236
+ if (flag === '--to' && isValue(value) && rest.length === 0) {
237
+ return { finish: false, to: value }
238
+ }
239
+ if (flag === '--finish' && value === undefined) {
240
+ return { finish: true, board_before: null }
241
+ }
242
+ if (flag === '--finish' && value === '--board-before') {
243
+ const [board_before, ...extra] = rest
244
+ if (isValue(board_before) && extra.length === 0) {
245
+ return { finish: true, board_before }
246
+ }
247
+ }
248
+ return undefined
89
249
  }
90
250
 
251
+ /** Doctor's computer checks of the Luca on disk now. */
252
+ const computerChecksOf =
253
+ ({ home, paseo }: { home: string; paseo: Paseo }) =>
254
+ async () =>
255
+ computerChecks({
256
+ home,
257
+ // Read again: an install changes the version on disk.
258
+ ...(await lucaInstall()),
259
+ computer: computerOf({ home }),
260
+ muninn: muninnOf({ home }),
261
+ muninn_health: muninnHealth,
262
+ claude: claudeOf({ home }),
263
+ paseo,
264
+ env: process.env,
265
+ })
266
+
91
267
  /** Runs `luca upgrade` with the flags after `upgrade`; returns the exit code. */
92
268
  export const upgradeCommand = async ({
93
269
  argv,
94
270
  }: {
95
271
  argv: string[]
96
272
  }): Promise<number> => {
97
- const to = parseTo(argv)
98
- if (to === undefined) {
273
+ const args = parseUpgradeArgs({ argv })
274
+ if (args === undefined) {
99
275
  console.error(UPGRADE_USAGE)
100
276
  return 2
101
277
  }
102
278
  const home = homedir()
103
279
  const paseo = paseoOf({ client_id: 'luca-upgrade' })
280
+ const log = (line: string) => {
281
+ console.log(line)
282
+ }
104
283
  try {
105
284
  const { luca_version, ...install } = await lucaInstall()
285
+ if (args.finish) {
286
+ const end = await finishUpgrade({
287
+ version: luca_version,
288
+ board_before: args.board_before,
289
+ home,
290
+ paseo,
291
+ ...install,
292
+ computer_checks: computerChecksOf({ home, paseo }),
293
+ log,
294
+ })
295
+ return end.ok ? 0 : 1
296
+ }
106
297
  const end = await runUpgrade({
107
- to,
298
+ to: args.to,
299
+ home,
108
300
  installed_version: luca_version,
109
301
  runs_dir: defaultRunsDir(),
110
302
  registry_path: defaultRegistryPath({
111
303
  env: process.env,
112
304
  home_dir: home,
113
305
  }),
306
+ list_processes: listProcesses,
114
307
  npm: npmOf(),
115
308
  bun: bunOf({ home }),
116
309
  paseo,
117
310
  ...install,
118
- computer_checks: async () =>
119
- computerChecks({
120
- home,
121
- // Read again: the install changed the version on disk.
122
- ...(await lucaInstall()),
123
- computer: computerOf({ home }),
124
- muninn: muninnOf({ home }),
125
- muninn_health: muninnHealth,
126
- claude: claudeOf({ home }),
127
- paseo,
128
- }),
129
- log: (line) => {
130
- console.log(line)
131
- },
311
+ new_install: newInstallOf({ home }),
312
+ computer_checks: computerChecksOf({ home, paseo }),
313
+ log,
132
314
  })
133
315
  return end.ok ? 0 : 1
134
316
  } catch (error) {
@@ -0,0 +1,149 @@
1
+ import { readdir } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ import { setUpBoard } from './board-in-paseo'
5
+ import type { PaseoPlugins } from './computer-adapters'
6
+ import {
7
+ formatChecks,
8
+ hasProblem,
9
+ reason,
10
+ type DoctorCheck,
11
+ } from './doctor-checks'
12
+ import { installLucaSkills } from './luca-skills'
13
+
14
+ /**
15
+ * The last steps of `luca upgrade`, after `bun add -g` (#529): reload the
16
+ * board, say when `/reload-skills` is needed, copy Luca's own skills, and
17
+ * run doctor's computer checks.
18
+ *
19
+ * They run in the newly installed Luca, not the one that installed it:
20
+ * `luca upgrade` hands off to the new install's `luca upgrade --finish`,
21
+ * so the board setup, the skills, and the checks are the new version's.
22
+ *
23
+ * Older Luca looks for this file next to the new install's `luca.ts` to
24
+ * tell whether the new install can finish an upgrade. Don't rename or move
25
+ * it.
26
+ */
27
+
28
+ /** How upgrade ended; `message` is the last line it printed. */
29
+ export type UpgradeEnd = { ok: boolean; message: string }
30
+
31
+ /**
32
+ * A fingerprint of every file in `dir` but its `node_modules`, or `null`
33
+ * when it can't be read. Short, so it fits on a command line.
34
+ */
35
+ export const boardFingerprint = async (dir: string): Promise<string | null> => {
36
+ const lines: string[] = []
37
+ const walk = async (at: string): Promise<void> => {
38
+ const entries = await readdir(at, { withFileTypes: true })
39
+ for (const entry of entries) {
40
+ const path = join(at, entry.name)
41
+ if (entry.isDirectory()) {
42
+ if (entry.name !== 'node_modules') await walk(path)
43
+ } else if (entry.isFile()) {
44
+ const bytes = await Bun.file(path).arrayBuffer()
45
+ lines.push(`${path}\t${Bun.hash(bytes)}`)
46
+ }
47
+ }
48
+ }
49
+ try {
50
+ await walk(dir)
51
+ } catch {
52
+ return null
53
+ }
54
+ return Bun.hash(lines.sort().join('\n')).toString(36)
55
+ }
56
+
57
+ /**
58
+ * Runs `luca upgrade`'s last steps for the installed `version`: reloads
59
+ * the board with its settings kept and its paths rewritten, says to run
60
+ * `/reload-skills` when the board's files differ from `board_before`,
61
+ * copies Luca's skills, then prints `computer_checks`. Ends with "Luca
62
+ * <version> is installed." Never throws.
63
+ *
64
+ * @example
65
+ * const end = await finishUpgrade({ version: '14.0.0-alpha.11', board_before, home, paseo, board_dir, skills_dir, engine_path, bun_path, computer_checks, log: console.log })
66
+ */
67
+ export const finishUpgrade = async ({
68
+ version,
69
+ board_before,
70
+ home,
71
+ paseo,
72
+ board_dir,
73
+ skills_dir,
74
+ engine_path,
75
+ bun_path,
76
+ computer_checks,
77
+ log,
78
+ }: {
79
+ /** The version just installed. */
80
+ version: string
81
+ /** The board folder's fingerprint before the install, or `null` when unknown. */
82
+ board_before: string | null
83
+ /** The home folder, whose `.claude/skills` gets Luca's own skills. */
84
+ home: string
85
+ paseo: PaseoPlugins
86
+ /** The board folder inside Luca's install folder. */
87
+ board_dir: string
88
+ /** The skills folder inside Luca's install folder. */
89
+ skills_dir: string
90
+ /** The real path of the installed `luca-run`. */
91
+ engine_path: string
92
+ /** Bun's own path. */
93
+ bun_path: string
94
+ /** Doctor's computer checks of the new install; none when not given. */
95
+ computer_checks?: () => Promise<DoctorCheck[]>
96
+ log: (line: string) => void
97
+ }): Promise<UpgradeEnd> => {
98
+ const say = ({ message, ok }: UpgradeEnd): UpgradeEnd => {
99
+ log(`[luca upgrade] ${message}`)
100
+ return { ok, message }
101
+ }
102
+ try {
103
+ const board = await setUpBoard({
104
+ command: 'luca upgrade',
105
+ paseo,
106
+ // Upgrade only reloads; `luca init` asks to turn plugins on.
107
+ ask: async () => false,
108
+ board_dir,
109
+ engine_path,
110
+ bun_path,
111
+ log,
112
+ })
113
+ if (board.ok) {
114
+ const after = await boardFingerprint(board_dir)
115
+ if (
116
+ board_before === null ||
117
+ after === null ||
118
+ board_before !== after
119
+ ) {
120
+ log(
121
+ "[luca upgrade] The board's files changed: run /reload-skills in a Paseo chat so its slash commands are up to date."
122
+ )
123
+ }
124
+ }
125
+ const skills = await installLucaSkills({
126
+ home,
127
+ skills_dir,
128
+ prefix: '[luca upgrade]',
129
+ log,
130
+ })
131
+ const end = say({
132
+ message: !board.ok
133
+ ? `Luca ${version} is installed, but the board wasn't reloaded. Run luca init to fix it.`
134
+ : !skills.ok
135
+ ? `Luca ${version} is installed, but its skills weren't copied. Run luca doctor --fix to fix it.`
136
+ : `Luca ${version} is installed.`,
137
+ ok: board.ok && skills.ok,
138
+ })
139
+ const checks = (await computer_checks?.()) ?? []
140
+ for (const line of formatChecks({ checks })) log(line)
141
+ return { ...end, ok: end.ok && !hasProblem({ checks }) }
142
+ } catch (error) {
143
+ // Such as computer checks that threw.
144
+ return say({
145
+ message: `Luca ${version} is installed, but its last steps failed: ${reason(error)}`,
146
+ ok: false,
147
+ })
148
+ }
149
+ }