@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,212 @@
1
+ import { join } from 'node:path'
2
+
3
+ /**
4
+ * The decision model's Cloudflare credentials (#534), from **Luca's own env
5
+ * file**, `$XDG_CONFIG_HOME/luca/.env` (or `~/.config/luca/.env`). The engine
6
+ * reads it itself, so it works the same for board-started and
7
+ * terminal-started runs. It never reads a repo's `.env`. A key set in the
8
+ * process env wins over the file, key by key, for tests and CI.
9
+ *
10
+ * ```
11
+ * CLOUDFLARE_ACCOUNT_ID=your-account-id
12
+ * CLOUDFLARE_API_TOKEN=your-workers-ai-token
13
+ * ```
14
+ *
15
+ * `CLOUDFLARE_AUTH_TOKEN` (the name Cloudflare's Clef page uses) works as the
16
+ * token too; `CLOUDFLARE_API_TOKEN` wins when both are set. Nothing here
17
+ * ever puts the token in a summary or a log line.
18
+ */
19
+
20
+ /** The Cloudflare account the decision model runs on. */
21
+ export const CLOUDFLARE_ACCOUNT_ID = 'CLOUDFLARE_ACCOUNT_ID'
22
+
23
+ /** The Workers AI token, the name wrangler and Cloudflare's tools use. */
24
+ export const CLOUDFLARE_API_TOKEN = 'CLOUDFLARE_API_TOKEN'
25
+
26
+ /** Another name for the token, from Cloudflare's Clef page. */
27
+ export const CLOUDFLARE_AUTH_TOKEN = 'CLOUDFLARE_AUTH_TOKEN'
28
+
29
+ /** The keys the decision model needs, in the order they are named. */
30
+ export const DECISION_MODEL_KEYS = [
31
+ CLOUDFLARE_ACCOUNT_ID,
32
+ CLOUDFLARE_API_TOKEN,
33
+ ] as const
34
+
35
+ export type DecisionModelKey = (typeof DECISION_MODEL_KEYS)[number]
36
+
37
+ /** A process env, or a stand-in for one. */
38
+ export type Env = Record<string, string | undefined>
39
+
40
+ /**
41
+ * The credentials, or the keys that are missing. `file` is Luca's env file,
42
+ * read or not.
43
+ */
44
+ export type DecisionModelCredentials =
45
+ | { ok: true; account_id: string; api_token: string; file: string }
46
+ | { ok: false; missing: DecisionModelKey[]; file: string }
47
+
48
+ /**
49
+ * Luca's own env file: `$XDG_CONFIG_HOME/luca/.env` when `XDG_CONFIG_HOME`
50
+ * is set and not empty, else `<home>/.config/luca/.env`. Pure.
51
+ *
52
+ * @example
53
+ * lucaEnvFile({ env: {}, home: '/Users/me' }) // '/Users/me/.config/luca/.env'
54
+ */
55
+ export const lucaEnvFile = ({
56
+ env,
57
+ home,
58
+ }: {
59
+ env: Env
60
+ home: string
61
+ }): string => {
62
+ const xdg = env.XDG_CONFIG_HOME ?? ''
63
+ return join(xdg === '' ? join(home, '.config') : xdg, 'luca', '.env')
64
+ }
65
+
66
+ /** A value without one pair of matching quotes around it. */
67
+ const unquote = (value: string): string => {
68
+ const first = value.at(0)
69
+ return value.length >= 2 &&
70
+ (first === '"' || first === "'") &&
71
+ value.at(-1) === first
72
+ ? value.slice(1, -1)
73
+ : value
74
+ }
75
+
76
+ /**
77
+ * The keys and values of an env file: `KEY=VALUE` lines, with an optional
78
+ * `export ` in front and the value trimmed and taken out of one pair of
79
+ * single or double quotes. Blank lines, `#` comments, and lines with no `=`
80
+ * or no key are skipped. No escapes and no `${...}`. Pure.
81
+ *
82
+ * @example
83
+ * parseEnvFile('# keys\nexport A="one two"\nB=2') // { A: 'one two', B: '2' }
84
+ */
85
+ export const parseEnvFile = (text: string): Record<string, string> => {
86
+ const values: Record<string, string> = {}
87
+ for (const raw of text.split(/\r?\n/)) {
88
+ const line = raw.trim()
89
+ if (line === '' || line.startsWith('#')) continue
90
+ const at = line.indexOf('=')
91
+ if (at === -1) continue
92
+ const key = line
93
+ .slice(0, at)
94
+ .trim()
95
+ .replace(/^export\s+/, '')
96
+ .trim()
97
+ if (key === '') continue
98
+ values[key] = unquote(line.slice(at + 1).trim())
99
+ }
100
+ return values
101
+ }
102
+
103
+ /** A file's text, or `null` when it is missing or can't be read. */
104
+ const readText = async (path: string): Promise<string | null> => {
105
+ try {
106
+ const file = Bun.file(path)
107
+ return (await file.exists()) ? await file.text() : null
108
+ } catch {
109
+ return null
110
+ }
111
+ }
112
+
113
+ /** The first of `values` that is set and not empty, or `null`. */
114
+ const firstSet = (values: (string | undefined)[]): string | null =>
115
+ values.find((value) => value !== undefined && value !== '') ?? null
116
+
117
+ /**
118
+ * The decision model's credentials: each key from the process env, else
119
+ * from Luca's env file (`lucaEnvFile`). An empty value is missing. Never
120
+ * throws: a missing or unreadable file reads as empty.
121
+ *
122
+ * @param env - The process env, such as `process.env`. Tests pass their own.
123
+ * @param home - The home folder, such as `homedir()`. Tests pass a temp one.
124
+ * @param read_file - Reads the file; `null` for none. Defaults to Bun's.
125
+ *
126
+ * @example
127
+ * const credentials = await loadDecisionModelCredentials({ env: process.env, home: homedir() })
128
+ * if (!credentials.ok) console.log(`Add ${credentials.missing.join(', ')} to ${credentials.file}`)
129
+ */
130
+ export const loadDecisionModelCredentials = async ({
131
+ env,
132
+ home,
133
+ read_file,
134
+ }: {
135
+ env: Env
136
+ home: string
137
+ read_file?: (path: string) => Promise<string | null>
138
+ }): Promise<DecisionModelCredentials> => {
139
+ const file = lucaEnvFile({ env, home })
140
+ const text = await (read_file ?? readText)(file).catch(() => null)
141
+ const values = text === null ? {} : parseEnvFile(text)
142
+ const account_id = firstSet([
143
+ env[CLOUDFLARE_ACCOUNT_ID],
144
+ values[CLOUDFLARE_ACCOUNT_ID],
145
+ ])
146
+ const api_token = firstSet([
147
+ env[CLOUDFLARE_API_TOKEN],
148
+ env[CLOUDFLARE_AUTH_TOKEN],
149
+ values[CLOUDFLARE_API_TOKEN],
150
+ values[CLOUDFLARE_AUTH_TOKEN],
151
+ ])
152
+ if (account_id !== null && api_token !== null) {
153
+ return { ok: true, account_id, api_token, file }
154
+ }
155
+ const missing = DECISION_MODEL_KEYS.filter((key) =>
156
+ key === CLOUDFLARE_ACCOUNT_ID ? account_id === null : api_token === null
157
+ )
158
+ return { ok: false, missing, file }
159
+ }
160
+
161
+ /**
162
+ * Which keys are set and which are missing, and the file: safe to print,
163
+ * since it never holds a value. Pure.
164
+ *
165
+ * @example
166
+ * describeDecisionModelCredentials(credentials)
167
+ * // 'CLOUDFLARE_ACCOUNT_ID set, CLOUDFLARE_API_TOKEN missing (Luca's env file: /Users/me/.config/luca/.env)'
168
+ */
169
+ export const describeDecisionModelCredentials = (
170
+ credentials: DecisionModelCredentials
171
+ ): string => {
172
+ const missing: string[] = credentials.ok ? [] : credentials.missing
173
+ const keys = DECISION_MODEL_KEYS.map(
174
+ (key) => `${key} ${missing.includes(key) ? 'missing' : 'set'}`
175
+ )
176
+ return `${keys.join(', ')} (Luca's env file: ${credentials.file})`
177
+ }
178
+
179
+ /**
180
+ * The lines to add to Luca's env file for the missing keys, with
181
+ * placeholders. Pure.
182
+ *
183
+ * @example
184
+ * missingKeyLines({ missing: ['CLOUDFLARE_API_TOKEN'] }) // ['CLOUDFLARE_API_TOKEN=your-workers-ai-token']
185
+ */
186
+ export const missingKeyLines = ({
187
+ missing,
188
+ }: {
189
+ missing: DecisionModelKey[]
190
+ }): string[] =>
191
+ missing.map((key) =>
192
+ key === CLOUDFLARE_ACCOUNT_ID
193
+ ? `${key}=your-account-id`
194
+ : `${key}=your-workers-ai-token`
195
+ )
196
+
197
+ /**
198
+ * One plain sentence saying the decision model has no credentials: the
199
+ * file and the keys to add. Never holds a value. Pure.
200
+ *
201
+ * @example
202
+ * noCredentialsText({ missing: ['CLOUDFLARE_API_TOKEN'], file: '/Users/me/.config/luca/.env' })
203
+ * // 'No Cloudflare credentials: add CLOUDFLARE_API_TOKEN to /Users/me/.config/luca/.env.'
204
+ */
205
+ export const noCredentialsText = ({
206
+ missing,
207
+ file,
208
+ }: {
209
+ missing: DecisionModelKey[]
210
+ file: string
211
+ }): string =>
212
+ `No Cloudflare credentials: add ${missing.join(' and ')} to ${file}.`
@@ -1,8 +1,17 @@
1
+ import chunk from 'lodash/chunk'
1
2
  import fromPairs from 'lodash/fromPairs'
2
3
 
3
- import type { JevFixed, JevJob, JevQuestion, JevRequest } from './jev-schemas'
4
+ import {
5
+ CLEF_MAX_QUESTIONS,
6
+ type JevFixed,
7
+ type JevJob,
8
+ type JevQuestion,
9
+ type JevRequest,
10
+ } from './jev-schemas'
11
+ import { offeredModels, type RoutedRole } from './model-pick'
4
12
 
5
13
  import type { Finding } from '../agents/role-results'
14
+ import { DEFAULT_MODEL, modelsOf } from '../config/engine-config'
6
15
  import type { EngineAction } from '../core/decide'
7
16
  import type { TicketSnapshot } from '../intake/intake-schemas'
8
17
  import type { JournalRecord } from '../journal/journal-record'
@@ -23,17 +32,15 @@ export type JevAsk = {
23
32
  role: string | null
24
33
  request: JevRequest
25
34
  fixed: JevFixed
35
+ /**
36
+ * Whether the answer can change what the engine does (#603); left out,
37
+ * a shadow ask.
38
+ */
39
+ acted?: boolean
26
40
  }
27
41
 
28
- /** The models Jev may pick for a ticket. */
29
- export const JEV_MODEL_OPTIONS = [
30
- 'claude-opus-5-5',
31
- 'claude-sonnet-5',
32
- 'claude-haiku-4-5',
33
- ] as const
34
-
35
- /** The model the engine really uses for every ticket. */
36
- export const JEV_FIXED_MODEL = 'claude-opus-5-5'
42
+ /** The model the engine falls back on, and every reviewer's. */
43
+ export const JEV_FIXED_MODEL = DEFAULT_MODEL
37
44
 
38
45
  /**
39
46
  * The stock skills Jev may give an agent. The engine gives agents none, so
@@ -133,42 +140,74 @@ const createTicketWorktreeAsks = ({
133
140
  })
134
141
  .join('\n')
135
142
  const base = ticketState({ state, ticket })
136
- return [
137
- {
138
- job: 'ticket_order',
139
- ticket,
140
- role: null,
141
- request: {
142
- state: { ...base, candidates: candidateList },
143
- questions: {
144
- next: choice({
145
- instructions:
146
- 'Which of these tickets should be built next?',
147
- options: candidates.map(({ number }) =>
148
- ticketKey(number)
149
- ),
150
- }),
151
- },
143
+ // With one ticket left the order is obvious, and Clef refuses a
144
+ // one-option choice (#546), so there's nothing to ask.
145
+ const orderAsk: JevAsk[] =
146
+ candidates.length < 2
147
+ ? []
148
+ : [
149
+ {
150
+ job: 'ticket_order',
151
+ ticket,
152
+ role: null,
153
+ request: {
154
+ state: { ...base, candidates: candidateList },
155
+ questions: {
156
+ next: choice({
157
+ instructions:
158
+ 'Which of these tickets should be built next?',
159
+ options: candidates.map(({ number }) =>
160
+ ticketKey(number)
161
+ ),
162
+ }),
163
+ },
164
+ },
165
+ fixed: { next: ticketKey(ticket) },
166
+ },
167
+ ]
168
+ return orderAsk
169
+ }
170
+
171
+ /**
172
+ * Which model a fresh agent of a routed role should run on: the ticket's
173
+ * facts, the role, and which try this is (#602). It offers the models at or
174
+ * above the role's floor (#603), and is acted on when that floor is below
175
+ * Opus; a floor of Opus is asked with all three, in shadow mode.
176
+ */
177
+ const agentModelAsk = ({
178
+ state,
179
+ ticket,
180
+ role,
181
+ attempt,
182
+ }: {
183
+ state: RunState
184
+ ticket: number | null
185
+ role: RoutedRole
186
+ attempt: number
187
+ }): JevAsk => {
188
+ const models =
189
+ state.config === null ? null : modelsOf({ config: state.config })
190
+ const floor = models?.floors[role] ?? JEV_FIXED_MODEL
191
+ return {
192
+ job: 'agent_model',
193
+ ticket,
194
+ role,
195
+ request: {
196
+ state: {
197
+ ...ticketState({ state, ticket }),
198
+ role,
199
+ try: String(attempt),
152
200
  },
153
- fixed: { next: ticketKey(ticket) },
154
- },
155
- {
156
- job: 'ticket_model',
157
- ticket,
158
- role: null,
159
- request: {
160
- state: base,
161
- questions: {
162
- model: choice({
163
- instructions:
164
- 'Which model should build this ticket? Pick the cheapest that will get it right.',
165
- options: JEV_MODEL_OPTIONS,
166
- }),
167
- },
201
+ questions: {
202
+ model: choice({
203
+ instructions: `Which model should the ${role} agent run on? Pick the cheapest that will get it right.`,
204
+ options: offeredModels({ floor }),
205
+ }),
168
206
  },
169
- fixed: { model: JEV_FIXED_MODEL },
170
207
  },
171
- ]
208
+ fixed: { model: JEV_FIXED_MODEL },
209
+ acted: floor !== JEV_FIXED_MODEL,
210
+ }
172
211
  }
173
212
 
174
213
  const agentSkillsAsk = ({
@@ -200,9 +239,19 @@ const agentSkillsAsk = ({
200
239
  ),
201
240
  })
202
241
 
242
+ /** The roles whose fresh agents get an `agent_model` ask on a ticket. */
243
+ const ROUTED_TICKET_ROLES: readonly RoutedRole[] = [
244
+ 'test-writer',
245
+ 'implementer',
246
+ ]
247
+
248
+ const isRoutedTicketRole = (role: string): role is RoutedRole =>
249
+ ROUTED_TICKET_ROLES.some((routed) => routed === role)
250
+
203
251
  /**
204
- * The asks to make before the engine carries out `action`: ticket order and
205
- * model before a ticket starts, and skills before each agent.
252
+ * The asks to make before the engine carries out `action`: ticket order
253
+ * before a ticket starts, skills before each agent, and the model before
254
+ * each fresh test-writer, implementer, and learner.
206
255
  */
207
256
  export const jevAsksBefore = ({
208
257
  action,
@@ -214,12 +263,30 @@ export const jevAsksBefore = ({
214
263
  switch (action.type) {
215
264
  case 'create_ticket_worktree':
216
265
  return createTicketWorktreeAsks({ state, ticket: action.ticket })
217
- case 'launch_agent':
266
+ case 'launch_agent': {
267
+ const { ticket, role } = action
268
+ const failed = state.tickets[ticket]?.failed_tries[role] ?? 0
269
+ return [
270
+ ...(isRoutedTicketRole(role)
271
+ ? [
272
+ agentModelAsk({
273
+ state,
274
+ ticket,
275
+ role,
276
+ attempt: failed + 1,
277
+ }),
278
+ ]
279
+ : []),
280
+ agentSkillsAsk({ state, ticket, role }),
281
+ ]
282
+ }
283
+ case 'launch_learner':
218
284
  return [
219
- agentSkillsAsk({
285
+ agentModelAsk({
220
286
  state,
221
- ticket: action.ticket,
222
- role: action.role,
287
+ ticket: null,
288
+ role: 'learner',
289
+ attempt: state.memory.learner.failed_tries + 1,
223
290
  }),
224
291
  ]
225
292
  default:
@@ -306,17 +373,33 @@ const findingText = ({ title, detail, file }: Finding): string =>
306
373
  .filter((part) => part !== null && part !== '')
307
374
  .join('\n')
308
375
 
376
+ /**
377
+ * The severity of each finding a ticket reviewer reported, at most
378
+ * `CLEF_MAX_QUESTIONS` findings to an ask, since Clef takes no more.
379
+ */
380
+ const findingSeverityAsks = ({
381
+ record,
382
+ state,
383
+ }: {
384
+ record: JournalRecord
385
+ state: RunState
386
+ }): JevAsk[] => {
387
+ if (record.kind !== 'agent_finished') return []
388
+ if (record.content.role !== 'ticket-reviewer') return []
389
+ return chunk(record.content.result.findings, CLEF_MAX_QUESTIONS).map(
390
+ (findings) => findingSeverityAsk({ record, state, findings })
391
+ )
392
+ }
393
+
309
394
  const findingSeverityAsk = ({
310
395
  record,
311
396
  state,
397
+ findings,
312
398
  }: {
313
399
  record: JournalRecord
314
400
  state: RunState
315
- }): JevAsk | null => {
316
- if (record.kind !== 'agent_finished') return null
317
- if (record.content.role !== 'ticket-reviewer') return null
318
- const { findings } = record.content.result
319
- if (findings.length === 0) return null
401
+ findings: Finding[]
402
+ }): JevAsk => {
320
403
  const key = (finding: Finding) => `finding_${questionId(finding.id)}`
321
404
  return {
322
405
  job: 'finding_severity',
@@ -361,9 +444,10 @@ export const jevAsksAfter = ({
361
444
  records: JournalRecord[]
362
445
  state: RunState
363
446
  }): JevAsk[] =>
364
- records.flatMap((record) =>
365
- [
366
- failureAsk({ record, state }),
367
- findingSeverityAsk({ record, state }),
368
- ].filter((ask): ask is JevAsk => ask !== null)
369
- )
447
+ records.flatMap((record) => {
448
+ const failure = failureAsk({ record, state })
449
+ return [
450
+ ...(failure === null ? [] : [failure]),
451
+ ...findingSeverityAsks({ record, state }),
452
+ ]
453
+ })
@@ -1,9 +1,12 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  /**
4
- * Jev, TypeSafe's labeling model, runs in **shadow mode**: the engine asks it
5
- * and journals its answers, but acts on its own fixed choices. These are the
6
- * shapes of what the engine asks and what it keeps of each answer.
4
+ * The **decision model** (Cloudflare's Clef today; Jev, TypeSafe's labeling
5
+ * model, before #534) runs in **shadow mode**: the engine asks it and
6
+ * journals its answers, but acts on its own fixed choices. These are the
7
+ * shapes of what the engine asks and what it keeps of each answer. The
8
+ * names (and the journal's `jev_*` record kinds) keep the Jev name, so old
9
+ * journals still read.
7
10
  */
8
11
 
9
12
  /** Pick one of the criteria's keys. */
@@ -44,6 +47,43 @@ export const JevRequestSchema = z.object({
44
47
 
45
48
  export type JevRequest = z.infer<typeof JevRequestSchema>
46
49
 
50
+ /** The most questions Clef takes in one request. */
51
+ export const CLEF_MAX_QUESTIONS = 64
52
+
53
+ /**
54
+ * A request Clef will take (#546): 1 to 64 questions, 2 to 255 options per
55
+ * choice, and 2 to 10 levels per score. Clef refuses anything else, so the
56
+ * engine checks each ask against this before sending it. Journals keep the
57
+ * looser `JevRequestSchema`, so old asks still read.
58
+ */
59
+ export const ClefRequestSchema = z.object({
60
+ state: z.record(z.string(), z.string()),
61
+ questions: z
62
+ .record(
63
+ z.string(),
64
+ z.discriminatedUnion('type', [
65
+ JevChoiceQuestionSchema.refine(
66
+ ({ criteria }) => {
67
+ const options = Object.keys(criteria).length
68
+ return options >= 2 && options <= 255
69
+ },
70
+ { message: 'A choice takes 2 to 255 options.' }
71
+ ),
72
+ JevScoreQuestionSchema,
73
+ JevNoulQuestionSchema,
74
+ ])
75
+ )
76
+ .refine(
77
+ (questions) => {
78
+ const count = Object.keys(questions).length
79
+ return count >= 1 && count <= CLEF_MAX_QUESTIONS
80
+ },
81
+ {
82
+ message: `A request takes 1 to ${CLEF_MAX_QUESTIONS} questions.`,
83
+ }
84
+ ),
85
+ })
86
+
47
87
  /**
48
88
  * What the engine keeps of one answer. `value` is the choice, score, or
49
89
  * probability Jev gave; `raw` is the answer exactly as it came back.
@@ -57,9 +97,14 @@ export const JevAnswerSchema = z.object({
57
97
 
58
98
  export type JevAnswer = z.infer<typeof JevAnswerSchema>
59
99
 
60
- /** The jobs the engine asks Jev about. */
100
+ /**
101
+ * The jobs the engine asks Jev about. `ticket_model` (one model per ticket)
102
+ * is no longer asked; `agent_model` (one per agent) replaced it in #602. It
103
+ * stays so old journals read.
104
+ */
61
105
  export const JevJobSchema = z.enum([
62
106
  'ticket_model',
107
+ 'agent_model',
63
108
  'agent_skills',
64
109
  'ticket_order',
65
110
  'failure_kind',
@@ -68,9 +113,17 @@ export const JevJobSchema = z.enum([
68
113
 
69
114
  export type JevJob = z.infer<typeof JevJobSchema>
70
115
 
71
- /** Why a Jev call gave no answers. */
116
+ /**
117
+ * Why a decision model call gave no answers. New calls write
118
+ * `missing_credentials` (no Cloudflare account id or token), `rejected`
119
+ * (Cloudflare turned the token down: HTTP 401 or 403), `timeout`, or
120
+ * `error`. `missing_key` is Jev's old reason (it had no key), kept so old
121
+ * journals read.
122
+ */
72
123
  export const JevFailureReasonSchema = z.enum([
73
124
  'missing_key',
125
+ 'missing_credentials',
126
+ 'rejected',
74
127
  'timeout',
75
128
  'error',
76
129
  ])
@@ -87,3 +140,31 @@ export const JevFixedSchema = z.record(
87
140
  )
88
141
 
89
142
  export type JevFixed = z.infer<typeof JevFixedSchema>
143
+
144
+ /** The Clef models on Workers AI the engine can ask (#534). */
145
+ export const DECISION_MODEL_IDS = [
146
+ '@cf/cloudflare/clef',
147
+ '@cf/cloudflare/clef-flash',
148
+ ] as const
149
+
150
+ /** A decision model's Workers AI id. */
151
+ export const DecisionModelIdSchema = z.enum(DECISION_MODEL_IDS)
152
+
153
+ export type DecisionModelId = z.infer<typeof DecisionModelIdSchema>
154
+
155
+ /** The model the engine asks when the config doesn't say: Clef, the accurate one. */
156
+ export const DEFAULT_DECISION_MODEL: DecisionModelId = '@cf/cloudflare/clef'
157
+
158
+ /**
159
+ * Why the decision model is off for a run: `no_credentials` (Luca's env
160
+ * file and the process env have no Cloudflare account id or token), or
161
+ * `rejected` (Cloudflare turned the token down on an ask).
162
+ */
163
+ export const DecisionModelOffReasonSchema = z.enum([
164
+ 'no_credentials',
165
+ 'rejected',
166
+ ])
167
+
168
+ export type DecisionModelOffReason = z.infer<
169
+ typeof DecisionModelOffReasonSchema
170
+ >