@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,70 @@
1
+ /**
2
+ * The decision model a real run asks (#534): what its setup holds
3
+ * (`DecisionModelSetup`), and opening it for a run (`openDecisionModel`), off
4
+ * with a journaled reason when there are no credentials.
5
+ */
6
+ import { decisionModelOf } from '../config/engine-config'
7
+ import { createClefClient, type DecisionModelFetch } from '../jev/clef-client'
8
+ import {
9
+ noCredentialsText,
10
+ type DecisionModelCredentials,
11
+ } from '../jev/decision-model-credentials'
12
+ import { DEFAULT_DECISION_MODEL } from '../jev/jev-schemas'
13
+ import type { JevShadow } from '../jev/jev-shadow'
14
+ import type { Journal } from '../journal/journal'
15
+ import { replayRun } from '../journal/replay'
16
+
17
+ /**
18
+ * What a real run needs to ask the decision model (#534): its Cloudflare
19
+ * credentials (`loadDecisionModelCredentials`), and for tests a fake
20
+ * `fetch` and a shorter timeout. The model comes from the run's config.
21
+ */
22
+ export type DecisionModelSetup = {
23
+ credentials: DecisionModelCredentials
24
+ /** Defaults to the global `fetch`. */
25
+ fetch?: DecisionModelFetch
26
+ /** Defaults to `DEFAULT_JEV_TIMEOUT_MS`. */
27
+ timeout_ms?: number
28
+ }
29
+
30
+ /**
31
+ * The decision model for this run, from `setup` and the model in the run's
32
+ * own config (its `run_started`, so a resume keeps it). With no credentials
33
+ * it is off for the run: one `decision_model_off { reason: 'no_credentials' }`
34
+ * record and one line naming Luca's env file and the keys to add, and no
35
+ * shadow, so nothing is ever asked. Never logs the token.
36
+ */
37
+ export const openDecisionModel = ({
38
+ setup,
39
+ journal,
40
+ log,
41
+ }: {
42
+ setup: DecisionModelSetup | undefined
43
+ journal: Journal
44
+ log: (line: string) => void
45
+ }): JevShadow | undefined => {
46
+ if (setup === undefined) return undefined
47
+ const { config } = replayRun({ records: journal.read() })
48
+ const model =
49
+ config === null ? DEFAULT_DECISION_MODEL : decisionModelOf({ config })
50
+ const { credentials } = setup
51
+ if (!credentials.ok) {
52
+ const detail = noCredentialsText(credentials)
53
+ journal.append({
54
+ kind: 'decision_model_off',
55
+ ticket: null,
56
+ role: null,
57
+ content: { model, reason: 'no_credentials', detail },
58
+ })
59
+ log(`[luca-run] decision model off: ${detail}`)
60
+ return undefined
61
+ }
62
+ log(`[luca-run] decision model: Clef (${model}), in shadow mode`)
63
+ return {
64
+ client: createClefClient({ credentials, model, fetch: setup.fetch }),
65
+ timeout_ms: setup.timeout_ms,
66
+ log: (line) => {
67
+ log(`[luca-run] ${line}`)
68
+ },
69
+ }
70
+ }
@@ -65,6 +65,7 @@ export const doctorCommand = async ({
65
65
  memory,
66
66
  },
67
67
  tmp_dir: '/tmp',
68
+ env: process.env,
68
69
  log: (line) => {
69
70
  console.log(line)
70
71
  },
@@ -14,6 +14,7 @@ import {
14
14
  reason,
15
15
  type DoctorCheck,
16
16
  } from './doctor-checks'
17
+ import { installLucaSkills } from './luca-skills'
17
18
  import { ensureMuninnEntry, hideToken } from './muninn-entry'
18
19
  import { repoChecks, runSetup, type SetupGitHub } from './setup'
19
20
  import {
@@ -24,6 +25,7 @@ import {
24
25
  } from './v13-leftovers'
25
26
 
26
27
  import type { V13Manifest } from '../doctor/v13-manifest'
28
+ import type { Env } from '../jev/decision-model-credentials'
27
29
  import type { MemoryClient } from '../memory/memory-client'
28
30
  import { runCommand } from '../shell/run-command'
29
31
 
@@ -31,13 +33,14 @@ import { runCommand } from '../shell/run-command'
31
33
  * `luca doctor [--fix]`: checks this computer, the repo when run inside
32
34
  * one, and what old Luca v13 left behind, and prints each check as OK, or
33
35
  * the problem and its exact fix. It exits 1 when any check is a problem;
34
- * warnings (memory off, planning-skill drift) don't fail it. When it finds
36
+ * warnings (memory off, the decision model off, planning-skill drift)
37
+ * don't fail it. When it finds
35
38
  * v13 leftovers, it links the migration guide.
36
39
  *
37
40
  * `--fix` fixes what's safe, without asking: it cleans up v13's leftovers
38
41
  * first (see `fixV13`), starts MuninnDB and repairs Claude Code's `muninn`
39
42
  * entry (as `luca init` does), reloads the board and rewrites its paths,
40
- * and runs `luca setup` in a repo. It never deletes and never commits: it
43
+ * installs Luca's own skills again, and runs `luca setup` in a repo. It never deletes and never commits: it
41
44
  * lists the repo files to commit. Installs, sign-ins, Paseo's plugin
42
45
  * consent, and other `luca` copies are only reported.
43
46
  */
@@ -126,8 +129,10 @@ const tryFix = async ({
126
129
 
127
130
  /** Fixes what's safe on this computer, guided by the checks. */
128
131
  const fixComputer = async ({
132
+ home,
129
133
  checks,
130
134
  board_dir,
135
+ skills_dir,
131
136
  engine_path,
132
137
  bun_path,
133
138
  muninn,
@@ -136,8 +141,10 @@ const fixComputer = async ({
136
141
  paseo,
137
142
  log,
138
143
  }: {
144
+ home: string
139
145
  checks: DoctorCheck[]
140
146
  board_dir: string
147
+ skills_dir: string
141
148
  engine_path: string
142
149
  bun_path: string
143
150
  muninn: MuninnCli
@@ -190,6 +197,9 @@ const fixComputer = async ({
190
197
  log,
191
198
  })
192
199
  }
200
+ if (statusOf({ checks, name: 'luca_skills' }) === 'problem') {
201
+ await installLucaSkills({ home, skills_dir, prefix: PREFIX, log })
202
+ }
193
203
  }
194
204
 
195
205
  /** Runs `luca setup` in the repo. */
@@ -243,7 +253,7 @@ const listToCommit = async ({
243
253
  * then checks again. Never throws, and never shows the token.
244
254
  *
245
255
  * @example
246
- * const end = await runDoctor({ home: homedir(), fix: false, luca_version, board_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo, repo: null, tmp_dir: '/tmp', log: console.log })
256
+ * const end = await runDoctor({ home: homedir(), fix: false, luca_version, board_dir, skills_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo, repo: null, tmp_dir: '/tmp', log: console.log })
247
257
  * process.exit(end.exit_code)
248
258
  */
249
259
  export const runDoctor = async ({
@@ -251,6 +261,7 @@ export const runDoctor = async ({
251
261
  fix,
252
262
  luca_version,
253
263
  board_dir,
264
+ skills_dir,
254
265
  engine_path,
255
266
  bun_path,
256
267
  computer,
@@ -261,6 +272,7 @@ export const runDoctor = async ({
261
272
  repo,
262
273
  tmp_dir,
263
274
  v13_manifest,
275
+ env,
264
276
  log,
265
277
  }: LucaInstall & {
266
278
  home: string
@@ -276,6 +288,8 @@ export const runDoctor = async ({
276
288
  tmp_dir: string
277
289
  /** The v13 fingerprint list; defaults to the committed one. */
278
290
  v13_manifest?: V13Manifest
291
+ /** The process env, for the decision model's check. Left out, none. */
292
+ env?: Env
279
293
  log: (line: string) => void
280
294
  }): Promise<DoctorEnd> => {
281
295
  const check = async (): Promise<DoctorCheck[]> => [
@@ -283,6 +297,7 @@ export const runDoctor = async ({
283
297
  home,
284
298
  luca_version,
285
299
  board_dir,
300
+ skills_dir,
286
301
  engine_path,
287
302
  bun_path,
288
303
  computer,
@@ -290,6 +305,7 @@ export const runDoctor = async ({
290
305
  muninn_health,
291
306
  claude,
292
307
  paseo,
308
+ env,
293
309
  })),
294
310
  ...(repo === null
295
311
  ? []
@@ -331,8 +347,10 @@ export const runDoctor = async ({
331
347
  })
332
348
  }
333
349
  await fixComputer({
350
+ home,
334
351
  checks,
335
352
  board_dir,
353
+ skills_dir,
336
354
  engine_path,
337
355
  bun_path,
338
356
  muninn,
@@ -3,7 +3,12 @@ import { join } from 'node:path'
3
3
 
4
4
  import { z } from 'zod'
5
5
 
6
- import { readRunStart, unfinishedRuns } from './run-modes'
6
+ import {
7
+ BOARD_MAX_AUTO_RESTARTS,
8
+ liveRunIds,
9
+ type ListProcesses,
10
+ } from './live-runs'
11
+ import { readRunStart, restartableRuns, type RestartableRun } from './run-modes'
7
12
 
8
13
  import { boardStateDir } from '../board/board-state-dir'
9
14
  import { decide } from '../core/decide'
@@ -12,7 +17,9 @@ import { createJournal, runJournalPath } from '../journal/journal'
12
17
 
13
18
  /**
14
19
  * The runs that are going, for the commands that must not change Luca under
15
- * them (`luca upgrade`).
20
+ * them (`luca upgrade`) and for a new run's repo wait (#615), and the
21
+ * unfinished runs whose engine is gone, which can go on later on any
22
+ * version.
16
23
  */
17
24
 
18
25
  /**
@@ -27,13 +34,37 @@ export const defaultRegistryPath = ({
27
34
  home_dir: string
28
35
  }): string => join(boardStateDir({ env, home_dir }), 'runs.json')
29
36
 
30
- /** A run that is not over, by spec and repo. */
37
+ /**
38
+ * Why a run counts as going, or `engine_gone` for an unfinished run nothing
39
+ * runs now:
40
+ *
41
+ * - `engine_running`: `ps` shows its engine.
42
+ * - `board_will_restart`: its engine is gone, but the board plugin will
43
+ * restart it (its registry entry hasn't ended and has restarts left).
44
+ * - `not_checked`: `ps` failed, so it counts as going to be safe.
45
+ * - `engine_gone`: not running, and the board won't restart it.
46
+ */
47
+ export type RunState =
48
+ | 'engine_running'
49
+ | 'board_will_restart'
50
+ | 'not_checked'
51
+ | 'engine_gone'
52
+
53
+ /** An unfinished run, by spec and repo. */
31
54
  export type GoingRun = {
32
55
  run_id: string
33
56
  /** `null` for a demo run. */
34
57
  spec: number | null
35
58
  /** `null` in journals from before the repo was kept. */
36
59
  repo: string | null
60
+ /** Whether the board plugin started it (it is in the board's registry). */
61
+ board: boolean
62
+ state: RunState
63
+ /**
64
+ * When its `run_started` was journaled; `null` for a registry entry
65
+ * with no journal yet.
66
+ */
67
+ started_at: string | null
37
68
  }
38
69
 
39
70
  const RegistrySchema = z.object({
@@ -42,11 +73,15 @@ const RegistrySchema = z.object({
42
73
  run_id: z.string(),
43
74
  repo: z.string(),
44
75
  spec: z.number().nullable(),
76
+ demo: z.boolean().default(false),
45
77
  ended: z.unknown().nullable().default(null),
78
+ restarts: z.number().default(0),
46
79
  })
47
80
  ),
48
81
  })
49
82
 
83
+ type RegistryEntry = z.infer<typeof RegistrySchema>['runs'][number]
84
+
50
85
  /** Whether run `run_id` has a journal in `runs_dir` whose run is over. */
51
86
  const journalIsOver = ({
52
87
  runs_dir,
@@ -65,35 +100,16 @@ const journalIsOver = ({
65
100
  }
66
101
  }
67
102
 
68
- /**
69
- * The runs that are going: every journal in `runs_dir` whose run is not
70
- * over (limit waits and stuck runs waiting for a reply included), and every
71
- * run in the board registry at `registry_path` that has not ended and whose
72
- * journal, if any, is not over. An error when the registry can't be read.
73
- *
74
- * @example
75
- * const going = await goingRuns({ runs_dir: defaultRunsDir(), registry_path })
76
- * if (going.ok) console.log(going.runs.map(({ spec }) => spec))
77
- */
78
- export const goingRuns = async ({
79
- runs_dir,
103
+ /** The board registry's runs; none when it has no file yet. */
104
+ const readRegistry = async ({
80
105
  registry_path,
81
106
  }: {
82
- runs_dir: string
83
107
  registry_path: string
84
- }): Promise<{ ok: true; runs: GoingRun[] } | { ok: false; error: string }> => {
85
- const runs: GoingRun[] = unfinishedRuns({ runs_dir }).map((run_id) => {
86
- const start = readRunStart({ runs_dir, run_id })
87
- return start.ok
88
- ? {
89
- run_id,
90
- spec: start.start.spec_number,
91
- repo: start.start.repo,
92
- }
93
- : { run_id, spec: null, repo: null }
94
- })
108
+ }): Promise<
109
+ { ok: true; entries: RegistryEntry[] } | { ok: false; error: string }
110
+ > => {
95
111
  const file = Bun.file(registry_path)
96
- if (!(await file.exists())) return { ok: true, runs }
112
+ if (!(await file.exists())) return { ok: true, entries: [] }
97
113
  let parsed
98
114
  try {
99
115
  parsed = RegistrySchema.safeParse(JSON.parse(await file.text()))
@@ -109,22 +125,162 @@ export const goingRuns = async ({
109
125
  error: `The board's run registry ${registry_path} is not valid: ${z.prettifyError(parsed.error)}`,
110
126
  }
111
127
  }
112
- const known = new Set(runs.map(({ run_id }) => run_id))
113
- for (const entry of parsed.data.runs) {
128
+ return { ok: true, entries: parsed.data.runs }
129
+ }
130
+
131
+ /** An unfinished run before its engine is checked. */
132
+ type Candidate = {
133
+ run_id: string
134
+ spec: number | null
135
+ repo: string | null
136
+ started_at: string | null
137
+ /** What its journal says (`null` with no journal to go on from). */
138
+ reason: RestartableRun['reason'] | null
139
+ entry: RegistryEntry | null
140
+ }
141
+
142
+ /**
143
+ * Whether the board plugin will restart a run whose engine is gone: the
144
+ * plugin started it, its entry hasn't ended, it isn't a demo, it has
145
+ * restarts left, and its journal can go on. Pure.
146
+ */
147
+ const boardWillRestart = ({ candidate }: { candidate: Candidate }) =>
148
+ candidate.entry !== null &&
149
+ candidate.entry.ended === null &&
150
+ !candidate.entry.demo &&
151
+ candidate.entry.restarts < BOARD_MAX_AUTO_RESTARTS &&
152
+ candidate.reason === 'resumable'
153
+
154
+ /**
155
+ * The unfinished runs: every journal in `runs_dir` whose run is not over
156
+ * (limit waits and stuck runs waiting for a reply included), and every run
157
+ * in the board registry at `registry_path` that has not ended and whose
158
+ * journal, if any, is not over. Each is then checked with `ps` (see
159
+ * `liveRunIds`):
160
+ *
161
+ * - `runs`: the going ones. A run is going when its engine is running, or
162
+ * when its engine is gone but the board will restart it. When `ps` fails,
163
+ * every unfinished run is going, and `ps_error` says why.
164
+ * - `stopped`: the rest that can go on from their journal (a crash or a
165
+ * launcher stop). Nothing runs them now, so they can wait for a resume on
166
+ * any version. A billing stop never goes on, so it is in neither list.
167
+ *
168
+ * An error when the registry can't be read.
169
+ *
170
+ * @example
171
+ * const going = await goingRuns({ runs_dir: defaultRunsDir(), registry_path, list_processes: listProcesses })
172
+ * if (going.ok) console.log(going.runs.map(({ spec }) => spec))
173
+ */
174
+ export const goingRuns = async ({
175
+ runs_dir,
176
+ registry_path,
177
+ list_processes,
178
+ }: {
179
+ runs_dir: string
180
+ registry_path: string
181
+ list_processes: ListProcesses
182
+ }): Promise<
183
+ | {
184
+ ok: true
185
+ runs: GoingRun[]
186
+ stopped: GoingRun[]
187
+ /** Why `ps` failed, or `null` when it worked. */
188
+ ps_error: string | null
189
+ }
190
+ | { ok: false; error: string }
191
+ > => {
192
+ const registry = await readRegistry({ registry_path })
193
+ if (!registry.ok) return registry
194
+ const entries = new Map(
195
+ registry.entries.map((entry) => [entry.run_id, entry])
196
+ )
197
+
198
+ const candidates: Candidate[] = restartableRuns({ runs_dir }).map(
199
+ ({ run_id, reason }) => {
200
+ const start = readRunStart({ runs_dir, run_id })
201
+ const entry = entries.get(run_id) ?? null
202
+ return {
203
+ run_id,
204
+ spec: start.ok
205
+ ? start.start.spec_number
206
+ : (entry?.spec ?? null),
207
+ repo: start.ok ? start.start.repo : (entry?.repo ?? null),
208
+ started_at: start.ok ? start.start.started_at : null,
209
+ reason,
210
+ entry,
211
+ }
212
+ }
213
+ )
214
+ const known = new Set(candidates.map(({ run_id }) => run_id))
215
+ for (const entry of registry.entries) {
114
216
  if (entry.ended !== null || known.has(entry.run_id)) continue
115
217
  if (journalIsOver({ runs_dir, run_id: entry.run_id })) continue
116
218
  known.add(entry.run_id)
117
- runs.push({ run_id: entry.run_id, spec: entry.spec, repo: entry.repo })
219
+ candidates.push({
220
+ run_id: entry.run_id,
221
+ spec: entry.spec,
222
+ repo: entry.repo,
223
+ started_at: null,
224
+ reason: null,
225
+ entry,
226
+ })
227
+ }
228
+
229
+ const runOf = ({
230
+ candidate,
231
+ state,
232
+ }: {
233
+ candidate: Candidate
234
+ state: RunState
235
+ }): GoingRun => ({
236
+ run_id: candidate.run_id,
237
+ spec: candidate.spec,
238
+ repo: candidate.repo,
239
+ board: candidate.entry !== null,
240
+ state,
241
+ started_at: candidate.started_at,
242
+ })
243
+
244
+ let live: Set<string>
245
+ try {
246
+ live = liveRunIds({ command_lines: await list_processes() })
247
+ } catch (error) {
248
+ // Never guess: an upgrade under a live engine is far worse than a
249
+ // refused one.
250
+ return {
251
+ ok: true,
252
+ runs: candidates.map((candidate) =>
253
+ runOf({ candidate, state: 'not_checked' })
254
+ ),
255
+ stopped: [],
256
+ ps_error: error instanceof Error ? error.message : String(error),
257
+ }
258
+ }
259
+
260
+ const runs: GoingRun[] = []
261
+ const stopped: GoingRun[] = []
262
+ for (const candidate of candidates) {
263
+ if (live.has(candidate.run_id)) {
264
+ runs.push(runOf({ candidate, state: 'engine_running' }))
265
+ } else if (boardWillRestart({ candidate })) {
266
+ runs.push(runOf({ candidate, state: 'board_will_restart' }))
267
+ } else if (
268
+ candidate.reason === 'resumable' ||
269
+ candidate.reason === 'launcher_stopped'
270
+ ) {
271
+ stopped.push(runOf({ candidate, state: 'engine_gone' }))
272
+ }
118
273
  }
119
- return { ok: true, runs }
274
+ return { ok: true, runs, stopped, ps_error: null }
120
275
  }
121
276
 
122
277
  /**
123
- * One going run as a list line. Pure.
278
+ * One unfinished run as a list line, with why it is going when its engine
279
+ * isn't running. Pure.
124
280
  *
125
281
  * @example
126
- * describeRun({ run_id: 'r1', spec: 5, repo: '/code/app' })
282
+ * describeRun({ run_id: 'r1', spec: 5, repo: '/code/app', board: false, state: 'engine_running', started_at: null })
127
283
  * // '- spec #5 in /code/app (run r1)'
128
284
  */
129
- export const describeRun = ({ run_id, spec, repo }: GoingRun): string =>
130
- `- ${spec === null ? 'a demo' : `spec #${spec}`} in ${repo ?? 'an unknown repo'} (run ${run_id})`
285
+ export const describeRun = ({ run_id, spec, repo, state }: GoingRun): string =>
286
+ `- ${spec === null ? 'a demo' : `spec #${spec}`} in ${repo ?? 'an unknown repo'} (run ${run_id})${state === 'board_will_restart' ? ': its engine is gone, and the board will restart it' : ''}`
@@ -160,6 +160,7 @@ export const initCommand = async ({
160
160
  ask: askInTerminal,
161
161
  computer: computerOf({ home }),
162
162
  ...(await lucaInstall()),
163
+ env: process.env,
163
164
  log: (line) => {
164
165
  console.log(line)
165
166
  },
@@ -19,9 +19,12 @@ import {
19
19
  reason,
20
20
  type DoctorCheck,
21
21
  } from './doctor-checks'
22
+ import { installLucaSkills } from './luca-skills'
22
23
  import { ensureMuninnEntry, hideToken } from './muninn-entry'
23
24
  import { runSetup } from './setup'
24
25
 
26
+ import type { Env } from '../jev/decision-model-credentials'
27
+
25
28
  /**
26
29
  * `luca init`: sets up this computer for Luca, once, and is safe to run
27
30
  * again. It has three parts, each run even when an earlier one failed, and
@@ -50,6 +53,11 @@ import { runSetup } from './setup'
50
53
  * `mattpocock/skills` with the `skills` tool, skipping any already there.
51
54
  * `skip_skills` skips this part.
52
55
  *
56
+ * Luca's own skills: copies them (`/luca-unstick`, `/luca-retro`) from Luca's
57
+ * install folder into `~/.claude/skills`, over Luca's earlier copy (see
58
+ * `installLucaSkills`). They are part of Luca, so `skip_skills` doesn't
59
+ * skip them.
60
+ *
53
61
  * The repo: inside a git repo, after the computer's checks, it runs `luca
54
62
  * setup` for that repo (see `runSetup`), with the output setup gives on its
55
63
  * own. Outside one, it stops after the computer part.
@@ -264,7 +272,8 @@ const setUpMemory = async ({
264
272
 
265
273
  /**
266
274
  * Runs `luca init` against `home` with these adapters: memory, then the
267
- * board, then the planning skills, then doctor's computer checks, then
275
+ * board, then the planning skills, then Luca's own skills, then doctor's
276
+ * computer checks, then
268
277
  * `luca setup` in `repo` when there is one. Never throws: a failure is
269
278
  * logged (token hidden) and ends with `ok: false`, as does a check that is
270
279
  * a problem or a setup to-do.
@@ -287,8 +296,10 @@ export const runInit = async ({
287
296
  computer,
288
297
  luca_version,
289
298
  board_dir,
299
+ skills_dir,
290
300
  engine_path,
291
301
  bun_path,
302
+ env,
292
303
  log,
293
304
  }: LucaInstall & {
294
305
  home: string
@@ -304,6 +315,8 @@ export const runInit = async ({
304
315
  skills: SkillsTool
305
316
  ask: Ask
306
317
  computer: Computer
318
+ /** The process env, for the decision model's check. Left out, none. */
319
+ env?: Env
307
320
  log: (line: string) => void
308
321
  }): Promise<InitEnd> => {
309
322
  const memory = await setUpMemory({
@@ -330,10 +343,17 @@ export const runInit = async ({
330
343
  } else {
331
344
  planning = await setUpSkills({ skills, log })
332
345
  }
346
+ const own_skills = await installLucaSkills({
347
+ home,
348
+ skills_dir,
349
+ prefix: '[luca init]',
350
+ log,
351
+ })
333
352
  const doctor = await computerChecks({
334
353
  home,
335
354
  luca_version,
336
355
  board_dir,
356
+ skills_dir,
337
357
  engine_path,
338
358
  bun_path,
339
359
  computer,
@@ -341,6 +361,7 @@ export const runInit = async ({
341
361
  muninn_health,
342
362
  claude,
343
363
  paseo,
364
+ env,
344
365
  })
345
366
  for (const line of formatChecks({ checks: doctor })) log(line)
346
367
  const setup =
@@ -358,6 +379,7 @@ export const runInit = async ({
358
379
  memory.ok &&
359
380
  board.ok &&
360
381
  planning.ok &&
382
+ own_skills.ok &&
361
383
  !hasProblem({ checks: doctor }) &&
362
384
  (setup?.ok ?? true),
363
385
  memory: memory.memory,
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Which runs have a live engine process, from `ps`. The board plugin checks
3
+ * the same way (`liveRunIds` and `listProcesses` in
4
+ * `packages/board/server/`). The plugin never imports the engine and the
5
+ * engine never imports the plugin, so this is a copy: change both together.
6
+ */
7
+
8
+ /** Every running process's full command line. Rejects when it can't list them. */
9
+ export type ListProcesses = () => Promise<string[]>
10
+
11
+ /**
12
+ * How many times the board plugin restarts one run by itself before it gives
13
+ * up. A copy of the board's `MAX_AUTO_RESTARTS`: change both together.
14
+ */
15
+ export const BOARD_MAX_AUTO_RESTARTS = 3
16
+
17
+ /**
18
+ * The run ids that have a live engine, from every process's command line: a
19
+ * run is live when a command line has `--run-id <id>` or `--resume <id>` (or
20
+ * `--run-id=<id>`), as whole words, so one id never matches inside another.
21
+ * Pure.
22
+ *
23
+ * @example
24
+ * liveRunIds({ command_lines: ['bun luca-run.ts --resume r1 --repo /x'] }) // Set { 'r1' }
25
+ */
26
+ export const liveRunIds = ({
27
+ command_lines,
28
+ }: {
29
+ command_lines: string[]
30
+ }): Set<string> => {
31
+ const live = new Set<string>()
32
+ for (const line of command_lines) {
33
+ const tokens = line.split(/\s+/)
34
+ tokens.forEach((token, index) => {
35
+ const next = tokens[index + 1]
36
+ if ((token === '--run-id' || token === '--resume') && next) {
37
+ live.add(next)
38
+ }
39
+ const joined = /^--(?:run-id|resume)=(.+)$/.exec(token)
40
+ if (joined?.[1]) live.add(joined[1])
41
+ })
42
+ }
43
+ return live
44
+ }
45
+
46
+ /**
47
+ * Every running process's full command line, from `ps -axww -o command=`
48
+ * (`-ww` never cuts a line). Rejects when `ps` fails, so a caller can take
49
+ * the safe side rather than guess.
50
+ *
51
+ * @example
52
+ * const live = liveRunIds({ command_lines: await listProcesses() })
53
+ */
54
+ export const listProcesses: ListProcesses = async () => {
55
+ const child = Bun.spawn(['ps', '-axww', '-o', 'command='], {
56
+ stdout: 'pipe',
57
+ stderr: 'pipe',
58
+ })
59
+ const [stdout, stderr, exit_code] = await Promise.all([
60
+ new Response(child.stdout).text(),
61
+ new Response(child.stderr).text(),
62
+ child.exited,
63
+ ])
64
+ if (exit_code !== 0) {
65
+ throw new Error(
66
+ `ps exited with ${exit_code}: ${stderr.trim().slice(0, 300)}`
67
+ )
68
+ }
69
+ return stdout
70
+ .split('\n')
71
+ .map((line) => line.trim())
72
+ .filter((line) => line !== '')
73
+ }