@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
@@ -7,9 +7,13 @@
7
7
  * --run-id <id> --board-plugin luca-board (token in $LUCA_BOARD_TOKEN)
8
8
  *
9
9
  * A real run builds with real Claude agents (`createClaudeLauncher`: Claude
10
- * Opus 5.5, every guard on, paid by your Claude plan) and asks Jev in shadow
11
- * mode (`createTypeSafeJev`, which reads `TYPESAFE_API_KEY`; with no key each
12
- * ask is journaled as `missing_key` and the run goes on). Memory (#370) is
10
+ * Opus 5.5, every guard on, paid by your Claude plan) and asks the decision
11
+ * model, Cloudflare's Clef, in shadow mode (#534). Its credentials,
12
+ * `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, come from Luca's own
13
+ * env file, `~/.config/luca/.env` (or `$XDG_CONFIG_HOME/luca/.env`), with
14
+ * the process env winning key by key; never from the repo's `.env`. With
15
+ * none, the decision model is off for the run: one line and one
16
+ * `decision_model_off` record, and no asks. Memory (#370) is
13
17
  * MuninnDB over MCP, found through `LUCA_MUNINN_URL` and `LUCA_MUNINN_TOKEN`
14
18
  * or Claude Code's `mcpServers.muninn` in `~/.claude.json`; with neither,
15
19
  * the run goes on without memory.
@@ -26,6 +30,8 @@
26
30
  */
27
31
  import { homedir } from 'node:os'
28
32
 
33
+ import { defaultRegistryPath, goingRuns } from './going-runs'
34
+ import { listProcesses } from './live-runs'
29
35
  import { parseRunArgs, type RunArgs } from './run-args'
30
36
  import {
31
37
  DEMO_TURN_DELAY_MS,
@@ -39,7 +45,7 @@ import {
39
45
  import { createClaudeLauncher } from '../agents/claude-launcher'
40
46
  import { createBoardSync, type BoardSync } from '../board/board-sync'
41
47
  import { createPaseoBoardLink } from '../board/paseo-board-link'
42
- import { createTypeSafeJev } from '../jev/jev-client'
48
+ import { loadDecisionModelCredentials } from '../jev/decision-model-credentials'
43
49
  import { defaultRunsDir } from '../journal/journal'
44
50
  import {
45
51
  defaultSharedReadingsPath,
@@ -76,6 +82,28 @@ const memoryOf = async (): Promise<MemoryDeps | undefined> => {
76
82
  return { client: createMuninnMcpClient({ settings: found.settings }) }
77
83
  }
78
84
 
85
+ /** The decision model's setup: its credentials from Luca's own env file. */
86
+ const decisionModelSetup = async () => ({
87
+ credentials: await loadDecisionModelCredentials({
88
+ env: process.env,
89
+ home: homedir(),
90
+ }),
91
+ })
92
+
93
+ /**
94
+ * The unfinished runs on this computer, for the repo wait (#615): every
95
+ * journal in the runs folder and the board's registry, checked with `ps`.
96
+ */
97
+ const goingRunsNow = () =>
98
+ goingRuns({
99
+ runs_dir: defaultRunsDir(),
100
+ registry_path: defaultRegistryPath({
101
+ env: process.env,
102
+ home_dir: homedir(),
103
+ }),
104
+ list_processes: listProcesses,
105
+ })
106
+
79
107
  /**
80
108
  * The usage line (#434): the `luca-board` plugin's lines, and the readings
81
109
  * every run shares, both in Luca's state folder.
@@ -117,17 +145,22 @@ const run = async ({
117
145
  runs_dir: defaultRunsDir(),
118
146
  repo: args.repo,
119
147
  tracker: async ({ repo }) =>
120
- createGitHubTracker({ repo: await githubRepoOf({ repo }) }),
148
+ createGitHubTracker({
149
+ repo: await githubRepoOf({ repo }),
150
+ log,
151
+ }),
121
152
  launcher: createClaudeLauncher({}),
122
- jev: { client: createTypeSafeJev() },
153
+ decision_model: await decisionModelSetup(),
123
154
  memory: await memoryOf(),
124
155
  usage: usageLine(),
156
+ going_runs: goingRunsNow,
125
157
  board,
126
158
  log,
127
159
  })
128
160
  }
129
161
  const tracker = createGitHubTracker({
130
162
  repo: await githubRepoOf({ repo: args.repo }),
163
+ log,
131
164
  })
132
165
  return runSpec({
133
166
  spec_number: args.spec_number,
@@ -137,9 +170,11 @@ const run = async ({
137
170
  runs_dir: defaultRunsDir(),
138
171
  tracker,
139
172
  launcher: createClaudeLauncher({}),
140
- jev: { client: createTypeSafeJev() },
173
+ decision_model: await decisionModelSetup(),
141
174
  memory: await memoryOf(),
142
175
  usage: usageLine(),
176
+ going_runs: goingRunsNow,
177
+ alongside: args.alongside === true,
143
178
  board,
144
179
  log,
145
180
  })
@@ -0,0 +1,183 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import type { StepEnd } from './board-in-paseo'
5
+ import { reason } from './doctor-checks'
6
+
7
+ /**
8
+ * Luca's own Claude Code skills, such as `/luca-unstick` (#504) and
9
+ * `/luca-retro` (#514). They ship in the package's `skills/` folder, next
10
+ * to `engine/` and `board/` (`packages/engine/skills/` in the repo). `luca
11
+ * init` and `luca upgrade` copy them to `~/.claude/skills/<skill>/`, so
12
+ * they work in any repo and any Paseo chat, and `luca doctor` checks the
13
+ * copy is the one the installed Luca ships (`luca doctor --fix` copies it
14
+ * again).
15
+ *
16
+ * Which skills there are comes from that folder, not a list in the code
17
+ * (#529): every folder in it with a `SKILL.md`. So a skill new in a
18
+ * release is copied and checked even by code from before it.
19
+ *
20
+ * A copy writes every file the skill ships (not its tests), over Luca's own
21
+ * earlier copy. It never deletes, and never touches other skills.
22
+ */
23
+
24
+ /** Test files stay in the repo: they aren't part of a skill. */
25
+ const isTestFile = (path: string) => /\.test\.[cm]?[jt]sx?$/.test(path)
26
+
27
+ /** Where Claude Code finds a user's own skills. */
28
+ export const claudeSkillsDir = ({ home }: { home: string }): string =>
29
+ join(home, '.claude', 'skills')
30
+
31
+ /**
32
+ * Luca's own skills in `skills_dir`, by folder name, sorted: each folder
33
+ * with a `SKILL.md`. None when the folder is missing.
34
+ *
35
+ * @example
36
+ * await lucaSkills({ skills_dir }) // ['luca-retro', 'luca-unstick']
37
+ */
38
+ export const lucaSkills = async ({
39
+ skills_dir,
40
+ }: {
41
+ skills_dir: string
42
+ }): Promise<string[]> => {
43
+ try {
44
+ const manifests = await Array.fromAsync(
45
+ new Bun.Glob('*/SKILL.md').scan({
46
+ cwd: skills_dir,
47
+ onlyFiles: true,
48
+ })
49
+ )
50
+ return manifests.map((manifest) => dirname(manifest)).toSorted()
51
+ } catch {
52
+ return []
53
+ }
54
+ }
55
+
56
+ /**
57
+ * The files one skill ships, relative to its folder in `skills_dir`,
58
+ * sorted: every file but tests. None when the folder is missing.
59
+ *
60
+ * @example
61
+ * await skillFiles({ skills_dir, skill: 'luca-unstick' }) // ['SKILL.md', 'scripts/stuck-summary.ts']
62
+ */
63
+ export const skillFiles = async ({
64
+ skills_dir,
65
+ skill,
66
+ }: {
67
+ skills_dir: string
68
+ skill: string
69
+ }): Promise<string[]> => {
70
+ try {
71
+ const files = await Array.fromAsync(
72
+ new Bun.Glob('**/*').scan({
73
+ cwd: join(skills_dir, skill),
74
+ dot: true,
75
+ onlyFiles: true,
76
+ })
77
+ )
78
+ return files.filter((file) => !isTestFile(file)).toSorted()
79
+ } catch {
80
+ return []
81
+ }
82
+ }
83
+
84
+ /** How one installed skill differs from the one Luca ships. */
85
+ export type SkillDrift = {
86
+ skill: string
87
+ /** Shipped files the installed copy lacks. */
88
+ missing: string[]
89
+ /** Shipped files whose installed copy has other content. */
90
+ changed: string[]
91
+ }
92
+
93
+ /**
94
+ * How each of Luca's skills in `~/.claude/skills` differs from the ones in
95
+ * `skills_dir`; a skill that matches has empty lists. None when
96
+ * `skills_dir` has no skills. Read-only.
97
+ */
98
+ export const skillDrift = async ({
99
+ home,
100
+ skills_dir,
101
+ }: {
102
+ home: string
103
+ skills_dir: string
104
+ }): Promise<SkillDrift[]> => {
105
+ const drift: SkillDrift[] = []
106
+ for (const skill of await lucaSkills({ skills_dir })) {
107
+ const files = await skillFiles({ skills_dir, skill })
108
+ const missing: string[] = []
109
+ const changed: string[] = []
110
+ for (const file of files) {
111
+ const installed = Bun.file(
112
+ join(claudeSkillsDir({ home }), skill, file)
113
+ )
114
+ if (!(await installed.exists())) {
115
+ missing.push(file)
116
+ continue
117
+ }
118
+ const shipped = Bun.file(join(skills_dir, skill, file))
119
+ if ((await installed.text()) !== (await shipped.text())) {
120
+ changed.push(file)
121
+ }
122
+ }
123
+ drift.push({ skill, missing, changed })
124
+ }
125
+ return drift
126
+ }
127
+
128
+ /**
129
+ * Copies Luca's skills from `skills_dir` into `~/.claude/skills`, over
130
+ * Luca's own earlier copy: each shipped file whose installed copy is
131
+ * missing or differs is written. Logs one line after `prefix`. Never
132
+ * throws.
133
+ *
134
+ * @example
135
+ * await installLucaSkills({ home: homedir(), skills_dir, prefix: '[luca init]', log: console.log })
136
+ * // [luca init] Luca's skills: installed /luca-retro, /luca-unstick in ~/.claude/skills
137
+ */
138
+ export const installLucaSkills = async ({
139
+ home,
140
+ skills_dir,
141
+ prefix,
142
+ log,
143
+ }: {
144
+ home: string
145
+ skills_dir: string
146
+ prefix: string
147
+ log: (line: string) => void
148
+ }): Promise<StepEnd> => {
149
+ const say = ({ message, ok }: StepEnd): StepEnd => {
150
+ log(message)
151
+ return { ok, message }
152
+ }
153
+ try {
154
+ const drift = await skillDrift({ home, skills_dir })
155
+ if (drift.length === 0) {
156
+ return say({
157
+ message: `${prefix} Luca's skills: Luca's folder ${skills_dir} has none. Reinstall Luca.`,
158
+ ok: false,
159
+ })
160
+ }
161
+ const written: string[] = []
162
+ for (const { skill, missing, changed } of drift) {
163
+ for (const file of [...missing, ...changed]) {
164
+ const target = join(claudeSkillsDir({ home }), skill, file)
165
+ await mkdir(dirname(target), { recursive: true })
166
+ await Bun.write(target, Bun.file(join(skills_dir, skill, file)))
167
+ }
168
+ if (missing.length + changed.length > 0) written.push(skill)
169
+ }
170
+ return say({
171
+ message:
172
+ written.length === 0
173
+ ? `${prefix} Luca's skills: up to date`
174
+ : `${prefix} Luca's skills: installed ${written.map((skill) => `/${skill}`).join(', ')} in ${claudeSkillsDir({ home })}`,
175
+ ok: true,
176
+ })
177
+ } catch (error) {
178
+ return say({
179
+ message: `${prefix} Luca's skills: ${reason(error)}`,
180
+ ok: false,
181
+ })
182
+ }
183
+ }
@@ -9,6 +9,8 @@
9
9
  * luca upgrade [--to <version>] moves to another version of Luca
10
10
  * luca doctor [--fix] checks this computer and repo; --fix
11
11
  * fixes what's safe
12
+ * luca models scores each routed role's model from
13
+ * this repo's runs
12
14
  * luca hook <anything> does nothing, quietly
13
15
  *
14
16
  * `luca hook` is for old Luca (v13), whose global Claude Code hook runs
@@ -26,7 +28,8 @@ Commands:
26
28
  Set up this computer for Luca (once), then the repo you're in
27
29
  setup [--base <branch>] Get the repo in this folder ready for Luca
28
30
  upgrade [--to <version>] Move to the newest Luca on your channel, or to <version>
29
- doctor [--fix] Check this computer and repo; --fix fixes what's safe`
31
+ doctor [--fix] Check this computer and repo; --fix fixes what's safe
32
+ models Score each routed role's model from this repo's runs`
30
33
 
31
34
  const main = async (): Promise<number> => {
32
35
  const [command, ...rest] = Bun.argv.slice(2)
@@ -49,6 +52,10 @@ const main = async (): Promise<number> => {
49
52
  const { doctorCommand } = await import('./doctor-command')
50
53
  return doctorCommand({ argv: rest })
51
54
  }
55
+ case 'models': {
56
+ const { modelsCommand } = await import('./models-command')
57
+ return modelsCommand({ argv: rest })
58
+ }
52
59
  case '--help':
53
60
  case '-h':
54
61
  case 'help':
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `luca models`: scores each routed role's model from the ticket review's
3
+ * findings over this repo's runs, and prints the report (#605):
4
+ *
5
+ * luca models
6
+ *
7
+ * It only reads: it never writes config or journals. A run is this repo's
8
+ * when its `run_started` repo is the main checkout or git top folder of the
9
+ * current folder. Exits 0, 2 on bad flags.
10
+ */
11
+ import { existsSync, readdirSync, realpathSync } from 'node:fs'
12
+ import { basename, dirname, resolve } from 'node:path'
13
+
14
+ import { defaultRunsDir, readRecords, runJournalPath } from '../journal/journal'
15
+ import type { JournalRecord } from '../journal/journal-record'
16
+ import { formatModelReport, scoreModels } from '../models/model-score'
17
+
18
+ /** `luca models`'s usage line. */
19
+ export const MODELS_USAGE = 'Usage: luca models'
20
+
21
+ /** A path with its symlinks followed, or made absolute when it's gone. */
22
+ const realPath = (path: string): string => {
23
+ try {
24
+ return realpathSync(path)
25
+ } catch {
26
+ return resolve(path)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * The repos a run started from `cwd` may name, real paths: the main
32
+ * checkout (a worktree's too), then the git top folder. Outside git, `cwd`.
33
+ */
34
+ const currentRepos = ({ cwd }: { cwd: string }): string[] => {
35
+ const git = (...args: string[]) => {
36
+ const out = Bun.spawnSync(['git', '-C', cwd, ...args])
37
+ const said = out.stdout.toString().trim()
38
+ return out.exitCode === 0 && said !== '' ? said : null
39
+ }
40
+ const top = git('rev-parse', '--show-toplevel')
41
+ const common = git(
42
+ 'rev-parse',
43
+ '--path-format=absolute',
44
+ '--git-common-dir'
45
+ )
46
+ const main =
47
+ common !== null && basename(common) === '.git' ? dirname(common) : null
48
+ return [
49
+ ...new Set(
50
+ [main, top ?? cwd]
51
+ .filter((repo) => repo !== null)
52
+ .map((repo) => realPath(repo))
53
+ ),
54
+ ]
55
+ }
56
+
57
+ /**
58
+ * The records of every run in `runs_dir` that started in one of `repos`,
59
+ * oldest run id first. A journal that can't be read is left out, with a
60
+ * line on `warn`.
61
+ */
62
+ const repoRuns = ({
63
+ runs_dir,
64
+ repos,
65
+ warn,
66
+ }: {
67
+ runs_dir: string
68
+ repos: string[]
69
+ warn: (line: string) => void
70
+ }): JournalRecord[][] => {
71
+ if (!existsSync(runs_dir)) return []
72
+ const want = new Set(repos)
73
+ return readdirSync(runs_dir, { withFileTypes: true })
74
+ .filter((entry) => entry.isDirectory())
75
+ .map(({ name }) => name)
76
+ .toSorted()
77
+ .flatMap((run_id) => {
78
+ let records: JournalRecord[]
79
+ try {
80
+ records = readRecords({
81
+ file: runJournalPath({ runs_dir, run_id }),
82
+ })
83
+ } catch (error) {
84
+ warn(
85
+ `Left out run ${run_id}: ${error instanceof Error ? error.message : String(error)}`
86
+ )
87
+ return []
88
+ }
89
+ const [first] = records
90
+ return first?.kind === 'run_started' &&
91
+ first.content.repo !== null &&
92
+ want.has(realPath(first.content.repo))
93
+ ? [records]
94
+ : []
95
+ })
96
+ }
97
+
98
+ /** Runs `luca models` with the flags after `models`; returns the exit code. */
99
+ export const modelsCommand = ({ argv }: { argv: string[] }): number => {
100
+ if (argv.length > 0) {
101
+ console.error(MODELS_USAGE)
102
+ return 2
103
+ }
104
+ const runs_dir = defaultRunsDir()
105
+ const repos = currentRepos({ cwd: process.cwd() })
106
+ const runs = repoRuns({
107
+ runs_dir,
108
+ repos,
109
+ warn: (line) => {
110
+ console.error(`[luca models] ${line}`)
111
+ },
112
+ })
113
+ if (runs.length === 0) {
114
+ console.log(`No Luca runs of ${repos.join(' or ')} in ${runs_dir}.`)
115
+ return 0
116
+ }
117
+ console.log(formatModelReport({ report: scoreModels({ runs }) }))
118
+ return 0
119
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Reading a run's journal to go on from it: the records of a run in the runs
3
+ * folder (`readRun`), and its `run_started` (`readRunStart`, `RunStart`).
4
+ */
5
+ import { existsSync } from 'node:fs'
6
+
7
+ import { errorText } from './run-end'
8
+
9
+ import { createJournal, runJournalPath } from '../journal/journal'
10
+ import type { JournalRecord } from '../journal/journal-record'
11
+
12
+ /** What a run's `run_started` says: its spec, base branch, repo, and time. */
13
+ export type RunStart = {
14
+ spec_number: number
15
+ base_branch: string
16
+ /** `null` in journals from before the repo was kept. */
17
+ repo: string | null
18
+ /** When `run_started` was journaled. */
19
+ started_at: string
20
+ }
21
+
22
+ /**
23
+ * The records of run `run_id`, or why there are none to go on from: no
24
+ * journal, an empty one, or one that cannot be read. Never makes the run's
25
+ * folder.
26
+ */
27
+ export const readRun = ({
28
+ runs_dir,
29
+ run_id,
30
+ }: {
31
+ runs_dir: string
32
+ run_id: string
33
+ }): { ok: true; records: JournalRecord[] } | { ok: false; error: string } => {
34
+ const file = runJournalPath({ runs_dir, run_id })
35
+ if (!existsSync(file)) {
36
+ return { ok: false, error: `No run ${run_id}: there is no ${file}.` }
37
+ }
38
+ try {
39
+ const records = createJournal({ file }).read()
40
+ return records.length === 0
41
+ ? {
42
+ ok: false,
43
+ error: `Run ${run_id} has an empty journal (${file}), so there is nothing to go on from; start a new run with --spec.`,
44
+ }
45
+ : { ok: true, records }
46
+ } catch (error) {
47
+ return { ok: false, error: errorText(error) }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Reads run `run_id`'s `run_started`: the spec, base branch, and repo a
53
+ * resume goes on with. An error when the run has no journal, an empty one,
54
+ * or one that does not start with `run_started`.
55
+ *
56
+ * @example
57
+ * const start = readRunStart({ runs_dir: defaultRunsDir(), run_id })
58
+ * if (start.ok) console.log(start.start.spec_number)
59
+ */
60
+ export const readRunStart = ({
61
+ runs_dir,
62
+ run_id,
63
+ }: {
64
+ runs_dir: string
65
+ run_id: string
66
+ }): { ok: true; start: RunStart } | { ok: false; error: string } => {
67
+ const read = readRun({ runs_dir, run_id })
68
+ if (!read.ok) return read
69
+ const [first] = read.records
70
+ if (first?.kind !== 'run_started') {
71
+ return {
72
+ ok: false,
73
+ error: `Run ${run_id}'s journal does not start with run_started, so it is not a run.`,
74
+ }
75
+ }
76
+ const { spec_number, base_branch, repo } = first.content
77
+ return {
78
+ ok: true,
79
+ start: { spec_number, base_branch, repo, started_at: first.time },
80
+ }
81
+ }
@@ -10,7 +10,7 @@ export const BOARD_TOKEN_ENV = 'LUCA_BOARD_TOKEN'
10
10
 
11
11
  /** How to call `luca-run`, shown with every argument error. */
12
12
  export const RUN_USAGE = `Usage:
13
- luca-run --spec <n> [--repo <path>] [--run-id <id>] [--base <branch>] [--board-plugin <id>]
13
+ luca-run --spec <n> [--repo <path>] [--run-id <id>] [--base <branch>] [--alongside] [--board-plugin <id>]
14
14
  luca-run --demo [--run-id <id>] [--board-plugin <id>]
15
15
  luca-run --resume <run-id> [--repo <path>] [--board-plugin <id>]
16
16
  luca-run --unfinished
@@ -26,6 +26,8 @@ export const RUN_USAGE = `Usage:
26
26
  (for --resume, to the repo the run started in).
27
27
  --run-id <id> The run's id and folder name. Defaults to a new one.
28
28
  --base <branch> The branch the run starts from. Defaults to main.
29
+ --alongside Start now, even while an earlier run on the repo is
30
+ going. Without it, the run waits for that run first.
29
31
  --board-plugin <id> Send the journal to this Paseo board plugin; the
30
32
  per-run token comes from $${BOARD_TOKEN_ENV}.`
31
33
 
@@ -67,6 +69,7 @@ const resumeArgsSchema = ({ cwd }: { cwd: string }) =>
67
69
  demo: z.undefined('Give only one of --resume or --demo.'),
68
70
  run_id: z.undefined('--resume <run-id> is the run id.'),
69
71
  base: z.undefined('--resume takes the base from the journal.'),
72
+ alongside: z.undefined('--alongside is for a new run (--spec).'),
70
73
  })
71
74
  .refine(({ board_plugin, token }) => board_plugin === null || token, {
72
75
  message: BOARD_TOKEN_MESSAGE,
@@ -91,6 +94,7 @@ const unfinishedArgsSchema = z
91
94
  repo: z.undefined(UNFINISHED_ALONE),
92
95
  run_id: z.undefined(UNFINISHED_ALONE),
93
96
  base: z.undefined(UNFINISHED_ALONE),
97
+ alongside: z.undefined(UNFINISHED_ALONE),
94
98
  board_plugin: z.undefined(UNFINISHED_ALONE),
95
99
  })
96
100
  .transform((): RunArgs => ({ mode: 'unfinished' }))
@@ -107,26 +111,42 @@ const runArgsSchema = ({ cwd }: { cwd: string }) =>
107
111
  .transform((path) => resolve(cwd, path)),
108
112
  run_id: RunIdSchema.default(() => makeRunId()),
109
113
  base: z.string().min(1).nullable().default(null),
114
+ alongside: z.boolean().default(false),
110
115
  board_plugin: z.string().min(1).nullable().default(null),
111
116
  token: z.string().nullable().default(null),
112
117
  })
113
118
  .refine(({ spec, demo }) => (spec === undefined) === demo, {
114
119
  message: 'Give exactly one of --spec <n> or --demo.',
115
120
  })
121
+ .refine(({ demo, alongside }) => !(demo && alongside), {
122
+ message: '--alongside is for a run of a spec (--spec).',
123
+ })
116
124
  .refine(({ board_plugin, token }) => board_plugin === null || token, {
117
125
  message: BOARD_TOKEN_MESSAGE,
118
126
  })
119
127
  .transform(
120
- ({ spec, repo, run_id, base, board_plugin, token }): RunArgs => {
128
+ ({
129
+ spec,
130
+ repo,
131
+ run_id,
132
+ base,
133
+ alongside,
134
+ board_plugin,
135
+ token,
136
+ }): RunArgs => {
121
137
  const common = {
122
138
  repo,
123
139
  run_id,
124
140
  base_branch: base,
125
141
  board: boardOf({ board_plugin, token }),
126
142
  }
127
- return spec === undefined
128
- ? { mode: 'demo', ...common }
129
- : { mode: 'spec', spec_number: spec, ...common }
143
+ if (spec === undefined) return { mode: 'demo', ...common }
144
+ return {
145
+ mode: 'spec',
146
+ spec_number: spec,
147
+ ...common,
148
+ ...(alongside ? { alongside: true } : {}),
149
+ }
130
150
  }
131
151
  )
132
152
 
@@ -142,7 +162,12 @@ type RunArgsCommon = {
142
162
 
143
163
  /** What `luca-run` was asked to do. */
144
164
  export type RunArgs =
145
- | (RunArgsCommon & { mode: 'spec'; spec_number: number })
165
+ | (RunArgsCommon & {
166
+ mode: 'spec'
167
+ spec_number: number
168
+ /** Skip the repo wait (`--alongside`); left out, the run waits. */
169
+ alongside?: true
170
+ })
146
171
  | (RunArgsCommon & { mode: 'demo' })
147
172
  | {
148
173
  mode: 'resume'
@@ -189,6 +214,7 @@ export const parseRunArgs = ({
189
214
  repo: { type: 'string' },
190
215
  'run-id': { type: 'string' },
191
216
  base: { type: 'string' },
217
+ alongside: { type: 'boolean' },
192
218
  'board-plugin': { type: 'string' },
193
219
  },
194
220
  }).values
@@ -210,6 +236,7 @@ export const parseRunArgs = ({
210
236
  repo: values.repo,
211
237
  run_id: values['run-id'],
212
238
  base: values.base,
239
+ alongside: values.alongside,
213
240
  board_plugin: values['board-plugin'],
214
241
  token: env[BOARD_TOKEN_ENV],
215
242
  })