@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,218 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { afterAll, beforeAll, expect } from 'bun:test'
6
+
7
+ import type { DecisionModelClient } from './clef-client'
8
+ import type { JevAnswer, JevQuestion, JevRequest } from './jev-schemas'
9
+
10
+ import type { ScriptedTurn } from '../agents/scripted-launcher'
11
+ import type { JournalRecord } from '../journal/journal-record'
12
+ import { runPractice, type PracticeRun } from '../testing/practice-repo'
13
+
14
+ /**
15
+ * Helpers shared by the end-to-end shadow-mode tests
16
+ * (`jev-shadow.e2e.test.ts` and `jev-shadow-findings.e2e.test.ts`): the fake
17
+ * decision model that disagrees with everything, and the run it is compared
18
+ * against.
19
+ */
20
+
21
+ /** A shadow-mode record: an ask, its reply, or the decision model turning off. */
22
+ export const isJev = (record: JournalRecord): boolean =>
23
+ record.kind.startsWith('jev_') || record.kind === 'decision_model_off'
24
+
25
+ /**
26
+ * Launches with test-run timings (" [7.00ms]") removed and commit shas
27
+ * (which carry the time) masked: a fix-loop follow-up quotes test output,
28
+ * whose timings differ between two runs, and bun leaves the timing off a
29
+ * test that runs very fast, so a timing is dropped whether or not it's there.
30
+ */
31
+ const withoutTimings = (launches: PracticeRun['launches']) =>
32
+ launches.map((launch) => ({
33
+ ...launch,
34
+ prompt: launch.prompt
35
+ .replace(/ ?\[\d+(\.\d+)?m?s\]/g, '')
36
+ .replace(/\b[0-9a-f]{40}\b/g, '[sha]'),
37
+ }))
38
+
39
+ /**
40
+ * Record kinds in order, but the final review's lenses run at once, so
41
+ * the kinds between its start and its pass are sorted.
42
+ */
43
+ const kindsInOrder = (records: JournalRecord[]): string[] => {
44
+ const kinds = records
45
+ .filter((record) => !isJev(record))
46
+ .map(({ kind }) => kind)
47
+ const from = kinds.indexOf('final_review_started')
48
+ const to = kinds.indexOf('final_review_passed')
49
+ if (from === -1 || to === -1) return kinds
50
+ return [
51
+ ...kinds.slice(0, from + 1),
52
+ ...kinds.slice(from + 1, to).toSorted(),
53
+ ...kinds.slice(to),
54
+ ]
55
+ }
56
+
57
+ /**
58
+ * Launches in order, but the final review's lenses (which start at once, in
59
+ * any order) sorted by role, with their session numbers masked.
60
+ */
61
+ const launchesInOrder = (launches: PracticeRun['launches']) => {
62
+ const lens = (launch: PracticeRun['launches'][number]) =>
63
+ launch.role.endsWith('-lens')
64
+ return [
65
+ ...launches.filter((launch) => !lens(launch)),
66
+ ...launches
67
+ .filter(lens)
68
+ .map((launch) => ({
69
+ ...launch,
70
+ session_id: launch.session_id.replace(/-\d+$/, '-[n]'),
71
+ }))
72
+ .toSorted((left, right) => left.role.localeCompare(right.role)),
73
+ ]
74
+ }
75
+
76
+ /** The run as it would look without Jev: the same steps, prompts, and PR. */
77
+ export const expectSameRun = ({
78
+ run,
79
+ without,
80
+ }: {
81
+ run: PracticeRun
82
+ without: PracticeRun
83
+ }) => {
84
+ expect(run.action).toEqual(without.action)
85
+ expect(kindsInOrder(run.records)).toEqual(kindsInOrder(without.records))
86
+ expect(withoutTimings(launchesInOrder(run.launches))).toEqual(
87
+ withoutTimings(launchesInOrder(without.launches))
88
+ )
89
+ expect(run.tracker.pullRequests()).toEqual(without.tracker.pullRequests())
90
+ }
91
+
92
+ /** The model id the fake decision models say they are. */
93
+ export const FAKE_MODEL = '@cf/cloudflare/clef'
94
+
95
+ /**
96
+ * A Jev that answers every question, and never as the engine would: the
97
+ * last option of each choice, the top of each score, yes to every skill.
98
+ */
99
+ const disagreeingAnswer = (question: JevQuestion): JevAnswer => {
100
+ switch (question.type) {
101
+ case 'choice': {
102
+ const choice = Object.keys(question.criteria).at(-1) ?? null
103
+ return {
104
+ value: choice,
105
+ confidence: 0.7,
106
+ raw: { choice, confidence: 0.7 },
107
+ }
108
+ }
109
+ case 'score': {
110
+ const score = question.criteria.at(-1) ?? null
111
+ return {
112
+ value: score,
113
+ confidence: 0.6,
114
+ raw: { score, confidence: 0.6 },
115
+ }
116
+ }
117
+ case 'noul':
118
+ return { value: 0.95, confidence: null, raw: { probability: 0.95 } }
119
+ }
120
+ }
121
+
122
+ /** Whether a request has a choice Clef refuses: fewer than two options (#546). */
123
+ export const hasOneOptionChoice = (request: JevRequest): boolean =>
124
+ Object.values(request.questions).some(
125
+ (question) =>
126
+ question.type === 'choice' &&
127
+ Object.keys(question.criteria).length < 2
128
+ )
129
+
130
+ const disagreeingJev = (): DecisionModelClient & {
131
+ signals: AbortSignal[]
132
+ requests: JevRequest[]
133
+ } => {
134
+ const signals: AbortSignal[] = []
135
+ const requests: JevRequest[] = []
136
+ return {
137
+ signals,
138
+ requests,
139
+ model: FAKE_MODEL,
140
+ ask: async ({ request, signal }) => {
141
+ signals.push(signal)
142
+ requests.push(request)
143
+ return {
144
+ ok: true,
145
+ answers: Object.fromEntries(
146
+ Object.entries(request.questions).map(([id, question]) => [
147
+ id,
148
+ disagreeingAnswer(question),
149
+ ])
150
+ ),
151
+ }
152
+ },
153
+ }
154
+ }
155
+
156
+ type Asked = Extract<JournalRecord, { kind: 'jev_asked' }>
157
+ type Answered = Extract<JournalRecord, { kind: 'jev_answered' }>
158
+ type Failed = Extract<JournalRecord, { kind: 'jev_failed' }>
159
+
160
+ export const asked = (records: JournalRecord[]): Asked[] =>
161
+ records.filter((record): record is Asked => record.kind === 'jev_asked')
162
+
163
+ /** The record right after each ask: its answer or its failure. */
164
+ export const replyTo = (
165
+ records: JournalRecord[],
166
+ ask: Asked
167
+ ): Answered | Failed | undefined => {
168
+ const next = records[records.indexOf(ask) + 1]
169
+ return next?.kind === 'jev_answered' || next?.kind === 'jev_failed'
170
+ ? next
171
+ : undefined
172
+ }
173
+
174
+ export const SKILL_IDS = [
175
+ 'tdd',
176
+ 'systematic_debugging',
177
+ 'codebase_design',
178
+ 'domain_modeling',
179
+ ]
180
+
181
+ /** A practice run with Jev, and the same run without it, built once. */
182
+ export const shareRuns = ({ turns }: { turns: () => ScriptedTurn[] }) => {
183
+ const shared: {
184
+ roots: string[]
185
+ jev?: ReturnType<typeof disagreeingJev>
186
+ run?: PracticeRun
187
+ without?: PracticeRun
188
+ } = { roots: [] }
189
+ beforeAll(async () => {
190
+ const root = await mkdtemp(join(tmpdir(), 'luca-engine-jev-'))
191
+ shared.roots.push(root)
192
+ const jev = disagreeingJev()
193
+ shared.jev = jev
194
+ shared.run = await runPractice({
195
+ root: join(root, 'with-jev'),
196
+ turns: turns(),
197
+ jev: { client: jev },
198
+ })
199
+ shared.without = await runPractice({
200
+ root: join(root, 'without-jev'),
201
+ turns: turns(),
202
+ })
203
+ }, 120_000)
204
+ afterAll(async () => {
205
+ await Promise.all(
206
+ shared.roots.map((root) =>
207
+ rm(root, { recursive: true, force: true })
208
+ )
209
+ )
210
+ })
211
+ return () => {
212
+ const { jev, run, without } = shared
213
+ if (jev === undefined || run === undefined || without === undefined) {
214
+ throw new Error('The shared runs were not built.')
215
+ }
216
+ return { jev, run, without }
217
+ }
218
+ }
@@ -1,12 +1,12 @@
1
1
  import { z } from 'zod'
2
2
 
3
- import type { JevClient, JevReply } from './jev-client'
3
+ import type { DecisionModelClient, DecisionModelReply } from './clef-client'
4
4
  import type { JevAsk } from './jev-jobs'
5
- import { JevAnswerSchema } from './jev-schemas'
5
+ import { ClefRequestSchema, JevAnswerSchema } from './jev-schemas'
6
6
 
7
7
  import type { Journal } from '../journal/journal'
8
8
 
9
- /** How long the engine waits for one Jev answer before it moves on. */
9
+ /** How long the engine waits for one decision model answer before it moves on. */
10
10
  export const DEFAULT_JEV_TIMEOUT_MS = 10_000
11
11
 
12
12
  const JevAnswersSchema = z.record(z.string(), JevAnswerSchema)
@@ -15,26 +15,33 @@ const JevAnswersSchema = z.record(z.string(), JevAnswerSchema)
15
15
  * A reply whose answers don't fit the journal's schema (only a broken client
16
16
  * sends one) becomes an error reply, so appending it can never throw.
17
17
  */
18
- const checkedReply = (reply: JevReply): JevReply => {
18
+ const checkedReply = (reply: DecisionModelReply): DecisionModelReply => {
19
19
  if (!reply.ok) return reply
20
20
  const parsed = JevAnswersSchema.safeParse(reply.answers)
21
- if (parsed.success) return { ok: true, answers: parsed.data }
21
+ if (parsed.success) {
22
+ return { ok: true, answers: parsed.data, model: reply.model }
23
+ }
22
24
  return {
23
25
  ok: false,
24
26
  reason: 'error',
25
- error: `Jev's answers do not fit their schema:\n${z.prettifyError(parsed.error)}`,
27
+ error: `The decision model's answers do not fit their schema:\n${z.prettifyError(parsed.error)}`,
26
28
  }
27
29
  }
28
30
 
29
- /** Jev and how long to wait for it, as `runEngine` takes them. */
31
+ /**
32
+ * The decision model, how long to wait for it, and where to print the line
33
+ * when it turns off mid-run, as `runEngine` takes them.
34
+ */
30
35
  export type JevShadow = {
31
- client: JevClient
36
+ client: DecisionModelClient
32
37
  /** Defaults to `DEFAULT_JEV_TIMEOUT_MS`. */
33
38
  timeout_ms?: number
39
+ /** Gets one plain line when the decision model turns off mid-run. */
40
+ log?: (line: string) => void
34
41
  }
35
42
 
36
43
  /**
37
- * Calls Jev once, and always comes back: a throw becomes an error reply, and
44
+ * Calls the decision model once, and always comes back: a throw becomes an error reply, and
38
45
  * a call past the timeout becomes a timeout reply even if the client ignores
39
46
  * the abort signal.
40
47
  */
@@ -43,19 +50,19 @@ const askWithTimeout = async ({
43
50
  ask,
44
51
  timeout_ms,
45
52
  }: {
46
- jev: JevClient
53
+ jev: DecisionModelClient
47
54
  ask: JevAsk
48
55
  timeout_ms: number
49
- }): Promise<JevReply> => {
56
+ }): Promise<DecisionModelReply> => {
50
57
  const controller = new AbortController()
51
58
  let timer: ReturnType<typeof setTimeout> | undefined
52
- const timedOut = new Promise<JevReply>((resolve) => {
59
+ const timedOut = new Promise<DecisionModelReply>((resolve) => {
53
60
  timer = setTimeout(() => {
54
61
  controller.abort()
55
62
  resolve({
56
63
  ok: false,
57
64
  reason: 'timeout',
58
- error: `Jev did not answer within ${timeout_ms} ms.`,
65
+ error: `The decision model did not answer within ${timeout_ms} ms.`,
59
66
  })
60
67
  }, timeout_ms)
61
68
  })
@@ -64,10 +71,10 @@ const askWithTimeout = async ({
64
71
  jev.ask({ request: ask.request, signal: controller.signal })
65
72
  )
66
73
  .catch(
67
- (error: unknown): JevReply => ({
74
+ (error: unknown): DecisionModelReply => ({
68
75
  ok: false,
69
76
  reason: 'error',
70
- error: `Jev threw: ${String(error)}`,
77
+ error: `The decision model threw: ${String(error)}`,
71
78
  })
72
79
  )
73
80
  try {
@@ -77,73 +84,111 @@ const askWithTimeout = async ({
77
84
  }
78
85
  }
79
86
 
87
+ /** Asks the decision model about a step's asks, in shadow mode. */
88
+ export type ShadowAsker = (asks: JevAsk[]) => Promise<void>
89
+
80
90
  /**
81
- * Asks Jev each question in **shadow mode**: journals `jev_asked` with the
82
- * engine's fixed choice, then `jev_answered` or `jev_failed`, linked back by
83
- * `asked_seq`. Jev's answers change nothing. A Jev error, timeout, or missing
84
- * key is journaled and never thrown, so the run goes on.
91
+ * Asks the decision model in **shadow mode** for one run: for each ask,
92
+ * journals `jev_asked` with the engine's fixed choice and the model, then
93
+ * `jev_answered` (with the model that answered) or `jev_failed`, linked back
94
+ * by `asked_seq`. The answers change nothing here; the launch step reads an
95
+ * `agent_model` answer whose ask is `acted` (#603). An ask outside Clef's limits
96
+ * (`ClefRequestSchema`) is skipped: not sent and not journaled. An error,
97
+ * timeout, or missing credentials is journaled and never thrown, so the run
98
+ * goes on.
99
+ *
100
+ * The first ask Cloudflare turns down (`rejected`: HTTP 401 or 403) turns
101
+ * the decision model **off** for the rest of the run: one
102
+ * `decision_model_off { reason: 'rejected' }` record and one line to `log`,
103
+ * and no ask after it is journaled or sent. The off switch lives in this
104
+ * asker, so each run (each `runEngine`) has its own. A timeout or an error
105
+ * doesn't turn it off; the next ask tries again.
85
106
  *
86
107
  * @example
87
- * await askJevInShadow({
88
- * jev: createTypeSafeJev(),
89
- * journal,
90
- * asks: jevAsksBefore({ action, state }),
91
- * })
108
+ * const ask = shadowAsker({ jev: { client: createClefClient({ credentials, model }) }, journal })
109
+ * await ask(jevAsksBefore({ action, state }))
92
110
  */
93
- export const askJevInShadow = async ({
111
+ export const shadowAsker = ({
94
112
  jev,
95
113
  journal,
96
- asks,
97
- timeout_ms,
98
114
  }: {
99
- jev: JevClient
115
+ jev: JevShadow
100
116
  journal: Journal
101
- asks: JevAsk[]
102
- /** Defaults to `DEFAULT_JEV_TIMEOUT_MS`. */
103
- timeout_ms?: number
104
- }): Promise<void> => {
105
- for (const ask of asks) {
106
- const { job, ticket, role, request, fixed } = ask
107
- const asked = journal.append({
108
- kind: 'jev_asked',
109
- ticket,
110
- role,
111
- content: { job, request, fixed },
112
- })
113
- const started = performance.now()
114
- const reply = checkedReply(
115
- await askWithTimeout({
116
- jev,
117
- ask,
118
- timeout_ms: timeout_ms ?? DEFAULT_JEV_TIMEOUT_MS,
117
+ }): ShadowAsker => {
118
+ const { client, timeout_ms, log } = jev
119
+ let off = false
120
+ return async (asks) => {
121
+ for (const ask of asks) {
122
+ if (off) return
123
+ // Clef refuses it (a one-option choice, say), and the answer is
124
+ // obvious: no call and no record (#546).
125
+ if (!ClefRequestSchema.safeParse(ask.request).success) continue
126
+ const { job, ticket, role, request, fixed, acted } = ask
127
+ const asked = journal.append({
128
+ kind: 'jev_asked',
129
+ ticket,
130
+ role,
131
+ // Shadow mode unless the ask says its answer is acted on.
132
+ content: {
133
+ job,
134
+ request,
135
+ fixed,
136
+ model: client.model,
137
+ acted: acted ?? false,
138
+ },
119
139
  })
120
- )
121
- const ms = Math.round(performance.now() - started)
122
- if (reply.ok) {
140
+ const started = performance.now()
141
+ const reply = checkedReply(
142
+ await askWithTimeout({
143
+ jev: client,
144
+ ask,
145
+ timeout_ms: timeout_ms ?? DEFAULT_JEV_TIMEOUT_MS,
146
+ })
147
+ )
148
+ const ms = Math.round(performance.now() - started)
149
+ if (reply.ok) {
150
+ journal.append({
151
+ kind: 'jev_answered',
152
+ ticket,
153
+ role,
154
+ content: {
155
+ job,
156
+ asked_seq: asked.seq,
157
+ answers: reply.answers,
158
+ ms,
159
+ model: reply.model ?? client.model,
160
+ },
161
+ })
162
+ continue
163
+ }
123
164
  journal.append({
124
- kind: 'jev_answered',
165
+ kind: 'jev_failed',
125
166
  ticket,
126
167
  role,
127
168
  content: {
128
169
  job,
129
170
  asked_seq: asked.seq,
130
- answers: reply.answers,
171
+ reason: reply.reason,
172
+ error: reply.error,
131
173
  ms,
132
174
  },
133
175
  })
134
- continue
176
+ // Asks run at once on other tickets; only the first turns it off.
177
+ if (reply.reason === 'rejected' && !off) {
178
+ off = true
179
+ const detail = `${reply.error} No more asks this run.`
180
+ journal.append({
181
+ kind: 'decision_model_off',
182
+ ticket: null,
183
+ role: null,
184
+ content: {
185
+ model: client.model,
186
+ reason: 'rejected',
187
+ detail,
188
+ },
189
+ })
190
+ log?.(`decision model off: ${detail}`)
191
+ }
135
192
  }
136
- journal.append({
137
- kind: 'jev_failed',
138
- ticket,
139
- role,
140
- content: {
141
- job,
142
- asked_seq: asked.seq,
143
- reason: reply.reason,
144
- error: reply.error,
145
- ms,
146
- },
147
- })
148
193
  }
149
194
  }