@alecsibilia/luca 14.0.0-alpha.11 → 14.0.0-alpha.12

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @alecsibilia/luca
2
2
 
3
+ ## 14.0.0-alpha.12
4
+
5
+ No changes in this release.
6
+
3
7
  ## 14.0.0-alpha.11
4
8
 
5
9
  No changes in this release.
@@ -1327,7 +1327,7 @@ export const BoardPanel = ({
1327
1327
  ) : null}
1328
1328
  {state.jev.asked > 0 ? (
1329
1329
  <Text style={styles.small}>
1330
- Jev, in shadow mode: {state.jev.asked} asked,{' '}
1330
+ Decision model, in shadow mode: {state.jev.asked} asked,{' '}
1331
1331
  {state.jev.answered} answered, {state.jev.failed} without an
1332
1332
  answer. The engine doesn't act on them.
1333
1333
  </Text>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luca/board",
3
- "version": "14.0.0-alpha.11",
3
+ "version": "14.0.0-alpha.12",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -304,10 +304,17 @@ export const BOARD_VOCABULARY = {
304
304
  worktrees_removed: z.looseObject({
305
305
  paths: z.array(z.string()).catch([]),
306
306
  }),
307
- // Jev in shadow mode: counted, never acted on.
307
+ // The decision model (Clef; Jev in older journals) in shadow mode:
308
+ // counted, never acted on.
308
309
  jev_asked: JevJobSchema,
309
310
  jev_answered: JevJobSchema,
310
311
  jev_failed: JevJobSchema,
312
+ /**
313
+ * The decision model is off for the rest of the run (#534): no
314
+ * credentials, or Cloudflare turned them down. The board reads only the
315
+ * plain-words `detail`.
316
+ */
317
+ decision_model_off: z.looseObject({ detail: z.string().catch('') }),
311
318
  // Agent messages: sent inside an agent's turn, so they move nothing.
312
319
  agent_message: z.looseObject({
313
320
  from: z.string(),
@@ -490,6 +497,7 @@ const BoardEntrySchema = z.discriminatedUnion('kind', [
490
497
  entry({ kind: 'jev_asked' }),
491
498
  entry({ kind: 'jev_answered' }),
492
499
  entry({ kind: 'jev_failed' }),
500
+ entry({ kind: 'decision_model_off' }),
493
501
  entry({ kind: 'agent_message' }),
494
502
  entry({ kind: 'agent_message_delivered' }),
495
503
  entry({ kind: 'shared_git_changed' }),
@@ -1,2 +1,2 @@
1
1
  /** The Luca version of this board, stamped when the package was packed. */
2
- export const LUCA_VERSION: string = "14.0.0-alpha.11"
2
+ export const LUCA_VERSION: string = "14.0.0-alpha.12"
@@ -227,8 +227,8 @@ const nothingToDoText = ({
227
227
 
228
228
  /**
229
229
  * What a record means in one line, or `null` for records that are noise in
230
- * a chat (snapshots, session summaries, a clean leftover scan, Jev in shadow
231
- * mode, ...).
230
+ * a chat (snapshots, session summaries, a clean leftover scan, the decision
231
+ * model's asks in shadow mode, ...).
232
232
  */
233
233
  export const describeRecord = ({
234
234
  before,
@@ -257,6 +257,11 @@ export const describeRecord = ({
257
257
  text: `Config reloaded: ${record.content.changes.map(({ field }) => field).join(', ')} changed.`,
258
258
  tone: 'info',
259
259
  })
260
+ case 'decision_model_off':
261
+ return event({
262
+ text: `Decision model off: ${record.content.detail}`,
263
+ tone: 'info',
264
+ })
260
265
  case 'config_reload_refused':
261
266
  return event({
262
267
  text: `Config not reloaded, so the run keeps its own: ${record.content.reason.split('\n')[0] ?? ''}`,
@@ -114,6 +114,7 @@ const QUIET_KINDS = new Set([
114
114
  'jev_asked',
115
115
  'jev_answered',
116
116
  'jev_failed',
117
+ 'decision_model_off',
117
118
  'agent_message',
118
119
  'agent_message_delivered',
119
120
  'shared_git_changed',
@@ -1298,6 +1299,9 @@ const applyKind = ({
1298
1299
  ...state,
1299
1300
  jev: { ...state.jev, failed: state.jev.failed + 1 },
1300
1301
  }
1302
+ // The decision model turning off is only a chat line (#534).
1303
+ case 'decision_model_off':
1304
+ return state
1301
1305
  case 'agent_message': {
1302
1306
  const refused = record.content.status === 'refused'
1303
1307
  return {
@@ -482,8 +482,9 @@ export const RunInfoSchema = z.object({
482
482
  export type RunInfo = z.infer<typeof RunInfoSchema>
483
483
 
484
484
  /**
485
- * Jev in shadow mode: how often the engine asked it and what came back. The
486
- * engine never acts on the answers, so the board only counts them.
485
+ * The decision model (Clef; Jev in older journals) in shadow mode: how often
486
+ * the engine asked it and what came back. The engine never acts on the
487
+ * answers, so the board only counts them.
487
488
  */
488
489
  export const JevCountsSchema = z.object({
489
490
  asked: z.number().int().min(0),
@@ -26,14 +26,20 @@ import { claudeSkillsDir, skillDrift } from './luca-skills'
26
26
  import { isRight, rightEntry, userMuninnEntry } from './muninn-entry'
27
27
 
28
28
  import { LUCA_PACKAGE } from '../config/luca-version'
29
+ import {
30
+ loadDecisionModelCredentials,
31
+ missingKeyLines,
32
+ type Env,
33
+ } from '../jev/decision-model-credentials'
34
+ import { DEFAULT_DECISION_MODEL } from '../jev/jev-schemas'
29
35
  import { READY_LABEL } from '../tracker/tracker'
30
36
 
31
37
  /**
32
38
  * `luca doctor`'s checks of this computer, read-only: Bun, the `luca`
33
39
  * copies on the PATH, Claude Code, the `gh` login, Paseo and its plugins,
34
40
  * the board, MuninnDB and Claude Code's `muninn` entry, the planning
35
- * skills, and Luca's own skills. `luca init` and `luca upgrade` end with them too. The token is
36
- * never shown.
41
+ * skills, Luca's own skills, and the decision model's credentials. `luca
42
+ * init` and `luca upgrade` end with them too. No token is ever shown.
37
43
  */
38
44
 
39
45
  /** The oldest tool versions Luca works with. */
@@ -367,12 +373,32 @@ const checkLucaSkills = async ({
367
373
  )
368
374
  }
369
375
 
376
+ /**
377
+ * The decision model's Cloudflare credentials (#534), from Luca's own env
378
+ * file and the process env. With none it is off: a warning, not a problem,
379
+ * since runs go on without it. Never shows the token.
380
+ */
381
+ const checkDecisionModel = async ({
382
+ env,
383
+ home,
384
+ }: {
385
+ env: Env
386
+ home: string
387
+ }) => {
388
+ const credentials = await loadDecisionModelCredentials({ env, home })
389
+ if (credentials.ok) return ok(`Clef set up (${DEFAULT_DECISION_MODEL})`)
390
+ return warning({
391
+ detail: `Decision model off: no credentials (${credentials.missing.join(' and ')} not set in ${credentials.file}).`,
392
+ fix: `Add these lines to ${credentials.file}: ${missingKeyLines(credentials).join(', ')} (a Workers AI token from the Cloudflare dashboard), or leave the decision model off.`,
393
+ })
394
+ }
395
+
370
396
  /**
371
397
  * Runs the computer checks, in order, read-only. Never throws: a check that
372
398
  * fails becomes a problem.
373
399
  *
374
400
  * @example
375
- * const checks = await computerChecks({ home, luca_version, board_dir, skills_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo })
401
+ * const checks = await computerChecks({ home, luca_version, board_dir, skills_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo, env: process.env })
376
402
  */
377
403
  export const computerChecks = async ({
378
404
  home,
@@ -386,6 +412,7 @@ export const computerChecks = async ({
386
412
  muninn_health,
387
413
  claude,
388
414
  paseo,
415
+ env,
389
416
  }: {
390
417
  home: string
391
418
  /** The installed Luca's version. */
@@ -400,6 +427,11 @@ export const computerChecks = async ({
400
427
  muninn_health: MuninnHealth
401
428
  claude: ClaudeMcp
402
429
  paseo: Paseo
430
+ /**
431
+ * The process env, for the decision model's keys and `XDG_CONFIG_HOME`.
432
+ * Left out, none: only Luca's env file in `home` counts.
433
+ */
434
+ env?: Env
403
435
  }): Promise<DoctorCheck[]> => {
404
436
  // With Paseo down, its plugins and the board aren't asked about.
405
437
  let paseo_up = true
@@ -446,6 +478,10 @@ export const computerChecks = async ({
446
478
  'luca_skills',
447
479
  () => checkLucaSkills({ home, skills_dir, luca_version }),
448
480
  ],
481
+ [
482
+ 'decision_model',
483
+ () => checkDecisionModel({ env: env ?? {}, home }),
484
+ ],
449
485
  ],
450
486
  })
451
487
  }
@@ -65,6 +65,7 @@ export const doctorCommand = async ({
65
65
  memory,
66
66
  },
67
67
  tmp_dir: '/tmp',
68
+ env: process.env,
68
69
  log: (line) => {
69
70
  console.log(line)
70
71
  },
@@ -25,6 +25,7 @@ import {
25
25
  } from './v13-leftovers'
26
26
 
27
27
  import type { V13Manifest } from '../doctor/v13-manifest'
28
+ import type { Env } from '../jev/decision-model-credentials'
28
29
  import type { MemoryClient } from '../memory/memory-client'
29
30
  import { runCommand } from '../shell/run-command'
30
31
 
@@ -32,7 +33,8 @@ import { runCommand } from '../shell/run-command'
32
33
  * `luca doctor [--fix]`: checks this computer, the repo when run inside
33
34
  * one, and what old Luca v13 left behind, and prints each check as OK, or
34
35
  * the problem and its exact fix. It exits 1 when any check is a problem;
35
- * warnings (memory off, planning-skill drift) don't fail it. When it finds
36
+ * warnings (memory off, the decision model off, planning-skill drift)
37
+ * don't fail it. When it finds
36
38
  * v13 leftovers, it links the migration guide.
37
39
  *
38
40
  * `--fix` fixes what's safe, without asking: it cleans up v13's leftovers
@@ -270,6 +272,7 @@ export const runDoctor = async ({
270
272
  repo,
271
273
  tmp_dir,
272
274
  v13_manifest,
275
+ env,
273
276
  log,
274
277
  }: LucaInstall & {
275
278
  home: string
@@ -285,6 +288,8 @@ export const runDoctor = async ({
285
288
  tmp_dir: string
286
289
  /** The v13 fingerprint list; defaults to the committed one. */
287
290
  v13_manifest?: V13Manifest
291
+ /** The process env, for the decision model's check. Left out, none. */
292
+ env?: Env
288
293
  log: (line: string) => void
289
294
  }): Promise<DoctorEnd> => {
290
295
  const check = async (): Promise<DoctorCheck[]> => [
@@ -300,6 +305,7 @@ export const runDoctor = async ({
300
305
  muninn_health,
301
306
  claude,
302
307
  paseo,
308
+ env,
303
309
  })),
304
310
  ...(repo === null
305
311
  ? []
@@ -160,6 +160,7 @@ export const initCommand = async ({
160
160
  ask: askInTerminal,
161
161
  computer: computerOf({ home }),
162
162
  ...(await lucaInstall()),
163
+ env: process.env,
163
164
  log: (line) => {
164
165
  console.log(line)
165
166
  },
@@ -23,6 +23,8 @@ import { installLucaSkills } from './luca-skills'
23
23
  import { ensureMuninnEntry, hideToken } from './muninn-entry'
24
24
  import { runSetup } from './setup'
25
25
 
26
+ import type { Env } from '../jev/decision-model-credentials'
27
+
26
28
  /**
27
29
  * `luca init`: sets up this computer for Luca, once, and is safe to run
28
30
  * again. It has three parts, each run even when an earlier one failed, and
@@ -297,6 +299,7 @@ export const runInit = async ({
297
299
  skills_dir,
298
300
  engine_path,
299
301
  bun_path,
302
+ env,
300
303
  log,
301
304
  }: LucaInstall & {
302
305
  home: string
@@ -312,6 +315,8 @@ export const runInit = async ({
312
315
  skills: SkillsTool
313
316
  ask: Ask
314
317
  computer: Computer
318
+ /** The process env, for the decision model's check. Left out, none. */
319
+ env?: Env
315
320
  log: (line: string) => void
316
321
  }): Promise<InitEnd> => {
317
322
  const memory = await setUpMemory({
@@ -356,6 +361,7 @@ export const runInit = async ({
356
361
  muninn_health,
357
362
  claude,
358
363
  paseo,
364
+ env,
359
365
  })
360
366
  for (const line of formatChecks({ checks: doctor })) log(line)
361
367
  const setup =
@@ -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.
@@ -39,7 +43,7 @@ import {
39
43
  import { createClaudeLauncher } from '../agents/claude-launcher'
40
44
  import { createBoardSync, type BoardSync } from '../board/board-sync'
41
45
  import { createPaseoBoardLink } from '../board/paseo-board-link'
42
- import { createTypeSafeJev } from '../jev/jev-client'
46
+ import { loadDecisionModelCredentials } from '../jev/decision-model-credentials'
43
47
  import { defaultRunsDir } from '../journal/journal'
44
48
  import {
45
49
  defaultSharedReadingsPath,
@@ -76,6 +80,14 @@ const memoryOf = async (): Promise<MemoryDeps | undefined> => {
76
80
  return { client: createMuninnMcpClient({ settings: found.settings }) }
77
81
  }
78
82
 
83
+ /** The decision model's setup: its credentials from Luca's own env file. */
84
+ const decisionModelSetup = async () => ({
85
+ credentials: await loadDecisionModelCredentials({
86
+ env: process.env,
87
+ home: homedir(),
88
+ }),
89
+ })
90
+
79
91
  /**
80
92
  * The usage line (#434): the `luca-board` plugin's lines, and the readings
81
93
  * every run shares, both in Luca's state folder.
@@ -119,7 +131,7 @@ const run = async ({
119
131
  tracker: async ({ repo }) =>
120
132
  createGitHubTracker({ repo: await githubRepoOf({ repo }) }),
121
133
  launcher: createClaudeLauncher({}),
122
- jev: { client: createTypeSafeJev() },
134
+ decision_model: await decisionModelSetup(),
123
135
  memory: await memoryOf(),
124
136
  usage: usageLine(),
125
137
  board,
@@ -137,7 +149,7 @@ const run = async ({
137
149
  runs_dir: defaultRunsDir(),
138
150
  tracker,
139
151
  launcher: createClaudeLauncher({}),
140
- jev: { client: createTypeSafeJev() },
152
+ decision_model: await decisionModelSetup(),
141
153
  memory: await memoryOf(),
142
154
  usage: usageLine(),
143
155
  board,
@@ -9,7 +9,7 @@ import { z } from 'zod'
9
9
  import type { AgentLauncher } from '../agents/agent-launcher'
10
10
  import { createScriptedLauncher } from '../agents/scripted-launcher'
11
11
  import type { BoardSync } from '../board/board-sync'
12
- import { loadEngineConfig } from '../config/engine-config'
12
+ import { decisionModelOf, loadEngineConfig } from '../config/engine-config'
13
13
  import { lucaVersion } from '../config/luca-version'
14
14
  import { decide, type EngineAction } from '../core/decide'
15
15
  import {
@@ -21,10 +21,16 @@ import {
21
21
  } from '../core/execute'
22
22
  import { nothingToDoText } from '../core/left-out'
23
23
  import { createGitAdapter } from '../git/git-adapter'
24
- import { createTypeSafeJev } from '../jev/jev-client'
24
+ import { createClefClient, type DecisionModelFetch } from '../jev/clef-client'
25
+ import {
26
+ noCredentialsText,
27
+ type DecisionModelCredentials,
28
+ } from '../jev/decision-model-credentials'
29
+ import { DEFAULT_DECISION_MODEL } from '../jev/jev-schemas'
25
30
  import type { JevShadow } from '../jev/jev-shadow'
26
31
  import { createJournal, runJournalPath, type Journal } from '../journal/journal'
27
32
  import type { JournalRecord } from '../journal/journal-record'
33
+ import { replayRun } from '../journal/replay'
28
34
  import type { UsageLineDeps } from '../limits/usage-line'
29
35
  import type { MemoryDeps } from '../memory/memory-client'
30
36
  import {
@@ -242,11 +248,67 @@ export const configReloadText = ({
242
248
  }
243
249
  }
244
250
 
251
+ /**
252
+ * What a real run needs to ask the decision model (#534): its Cloudflare
253
+ * credentials (`loadDecisionModelCredentials`), and for tests a fake
254
+ * `fetch` and a shorter timeout. The model comes from the run's config.
255
+ */
256
+ export type DecisionModelSetup = {
257
+ credentials: DecisionModelCredentials
258
+ /** Defaults to the global `fetch`. */
259
+ fetch?: DecisionModelFetch
260
+ /** Defaults to `DEFAULT_JEV_TIMEOUT_MS`. */
261
+ timeout_ms?: number
262
+ }
263
+
264
+ /**
265
+ * The decision model for this run, from `setup` and the model in the run's
266
+ * own config (its `run_started`, so a resume keeps it). With no credentials
267
+ * it is off for the run: one `decision_model_off { reason: 'no_credentials' }`
268
+ * record and one line naming Luca's env file and the keys to add, and no
269
+ * shadow, so nothing is ever asked. Never logs the token.
270
+ */
271
+ const openDecisionModel = ({
272
+ setup,
273
+ journal,
274
+ log,
275
+ }: {
276
+ setup: DecisionModelSetup | undefined
277
+ journal: Journal
278
+ log: (line: string) => void
279
+ }): JevShadow | undefined => {
280
+ if (setup === undefined) return undefined
281
+ const { config } = replayRun({ records: journal.read() })
282
+ const model =
283
+ config === null ? DEFAULT_DECISION_MODEL : decisionModelOf({ config })
284
+ const { credentials } = setup
285
+ if (!credentials.ok) {
286
+ const detail = noCredentialsText(credentials)
287
+ journal.append({
288
+ kind: 'decision_model_off',
289
+ ticket: null,
290
+ role: null,
291
+ content: { model, reason: 'no_credentials', detail },
292
+ })
293
+ log(`[luca-run] decision model off: ${detail}`)
294
+ return undefined
295
+ }
296
+ log(`[luca-run] decision model: Clef (${model}), in shadow mode`)
297
+ return {
298
+ client: createClefClient({ credentials, model, fetch: setup.fetch }),
299
+ timeout_ms: setup.timeout_ms,
300
+ log: (line) => {
301
+ log(`[luca-run] ${line}`)
302
+ },
303
+ }
304
+ }
305
+
245
306
  /**
246
307
  * A real run of `spec_number` on `repo`: loads the repo's engine config,
247
308
  * opens (or resumes) the run's journal at `<runs_dir>/<run_id>`, and runs the
248
- * engine with the board kept in step, the agents from `launcher`, and Jev
249
- * in shadow mode if given. With `memory`, a new run turns memory on (#370),
309
+ * engine with the board kept in step, the agents from `launcher`, and the
310
+ * decision model in shadow mode if given (`decision_model`; with no
311
+ * credentials it is off for the run, see `openDecisionModel`). With `memory`, a new run turns memory on (#370),
250
312
  * with the engine config's `muninn.vault` as the project vault (none:
251
313
  * `default` only); a resumed run keeps what its journal says. A new run
252
314
  * stops on a bad config; a resume takes the config's build fields again
@@ -258,7 +320,8 @@ export const configReloadText = ({
258
320
  * const end = await runSpec({
259
321
  * spec_number: 374, repo: '/code/app', run_id, base_branch: null,
260
322
  * runs_dir: defaultRunsDir(), tracker: createGitHubTracker({ repo: 'acme/app' }),
261
- * launcher: createClaudeLauncher({}), jev: { client: createTypeSafeJev() },
323
+ * launcher: createClaudeLauncher({}),
324
+ * decision_model: { credentials: await loadDecisionModelCredentials({ env: process.env, home: homedir() }) },
262
325
  * board, log: console.log,
263
326
  * })
264
327
  */
@@ -270,7 +333,7 @@ export const runSpec = async ({
270
333
  runs_dir,
271
334
  tracker,
272
335
  launcher,
273
- jev,
336
+ decision_model,
274
337
  memory,
275
338
  usage,
276
339
  board,
@@ -285,8 +348,8 @@ export const runSpec = async ({
285
348
  runs_dir: string
286
349
  tracker: Tracker
287
350
  launcher: RunLauncher
288
- /** Jev in shadow mode. Leave it out to run without Jev. */
289
- jev?: JevShadow
351
+ /** The decision model's setup (#534). Leave it out to run without one. */
352
+ decision_model?: DecisionModelSetup
290
353
  /** MuninnDB (#370). Leave it out to run without memory. */
291
354
  memory?: MemoryDeps
292
355
  /** The usage line (#434). Leave it out to run without one. */
@@ -331,6 +394,7 @@ export const runSpec = async ({
331
394
  const reload = recordConfigReload({ journal, loaded })
332
395
  if (reload !== null) log(`[luca-run] ${configReloadText({ reload })}`)
333
396
  }
397
+ const jev = openDecisionModel({ setup: decision_model, journal, log })
334
398
  return driveRun({
335
399
  journal,
336
400
  run_id,
@@ -550,7 +614,7 @@ export const resumeRun = async ({
550
614
  repo,
551
615
  tracker,
552
616
  launcher,
553
- jev,
617
+ decision_model,
554
618
  memory,
555
619
  usage,
556
620
  board,
@@ -563,8 +627,8 @@ export const resumeRun = async ({
563
627
  repo: string | null
564
628
  tracker: ({ repo }: { repo: string }) => Tracker | Promise<Tracker>
565
629
  launcher: RunLauncher
566
- /** Jev in shadow mode. Leave it out to run without Jev. */
567
- jev?: JevShadow
630
+ /** The decision model's setup (#534). Leave it out to go on without one. */
631
+ decision_model?: DecisionModelSetup
568
632
  /**
569
633
  * MuninnDB (#370), used when the run's `run_started` turned memory on.
570
634
  * Leave it out to go on without memory.
@@ -603,7 +667,7 @@ export const resumeRun = async ({
603
667
  runs_dir,
604
668
  tracker: made,
605
669
  launcher,
606
- jev,
670
+ decision_model,
607
671
  memory,
608
672
  usage,
609
673
  board,
@@ -635,19 +699,12 @@ const slowLauncher = ({
635
699
  closeSession: (args) => launcher.closeSession(args),
636
700
  })
637
701
 
638
- /**
639
- * The demo's Jev: the real client with no key, so every ask is journaled as
640
- * `jev_failed` (`missing_key`) and nothing is sent, as in a real run without
641
- * `TYPESAFE_API_KEY`.
642
- */
643
- const OFFLINE_JEV: JevShadow = { client: createTypeSafeJev({ api_key: '' }) }
644
-
645
702
  /**
646
703
  * A practice run, safe to try the board with: a throwaway repo with a local
647
704
  * bare origin in a temp folder, the practice spec with two tickets (#12
648
705
  * blocked by #11) in an in-memory tracker, and scripted agents that take
649
- * `turn_delay_ms` per turn. No GitHub, no models, no network: Jev is asked in
650
- * shadow mode with no key, and memory (#370) is a fake MuninnDB seeded with
706
+ * `turn_delay_ms` per turn. No GitHub, no models, no network: the decision
707
+ * model is off (its credentials are never read), and memory (#370) is a fake MuninnDB seeded with
651
708
  * a few memories, so the board shows searches, the learner, and a save
652
709
  * with nothing leaving the machine. The temp folder (which also holds the run's
653
710
  * journal) is removed at the end.
@@ -681,6 +738,7 @@ export const runDemo = async ({
681
738
  log(`[luca-run] repo: ${repo}`)
682
739
  log(`[luca-run] origin: ${origin}`)
683
740
  log(`[luca-run] journal: ${journal.file}`)
741
+ log('[luca-run] decision model off: the demo never asks it.')
684
742
  const loaded = await loadEngineConfig({ repo_root: repo })
685
743
  if (!loaded.ok) throw new Error(loaded.error)
686
744
  const tracker = demoTracker()
@@ -714,7 +772,6 @@ export const runDemo = async ({
714
772
  tracker,
715
773
  git: createGitAdapter({ repo_root: repo }),
716
774
  launcher,
717
- jev: OFFLINE_JEV,
718
775
  memory,
719
776
  board: board ?? undefined,
720
777
  }),
@@ -261,6 +261,7 @@ const computerChecksOf =
261
261
  muninn_health: muninnHealth,
262
262
  claude: claudeOf({ home }),
263
263
  paseo,
264
+ env: process.env,
264
265
  })
265
266
 
266
267
  /** Runs `luca upgrade` with the flags after `upgrade`; returns the exit code. */
@@ -43,6 +43,8 @@ export const RELOADED_FIELDS = [
43
43
  * - `muninn`: the run's memory vault is fixed at start (`run_started.memory`).
44
44
  * - `run_budget_tokens`: the budget the run's tokens are counted against,
45
45
  * and the board's, from `run_started`; a raise is a `retry` reply.
46
+ * - `decision_model`: the model shadow mode asks (#534), so a run's asks
47
+ * are all of one model and can be scored together.
46
48
  */
47
49
  export const FROZEN_FIELDS = [
48
50
  'checks',
@@ -51,6 +53,7 @@ export const FROZEN_FIELDS = [
51
53
  'rule_files',
52
54
  'muninn',
53
55
  'run_budget_tokens',
56
+ 'decision_model',
54
57
  ] as const
55
58
 
56
59
  const positiveInt = z.number().int().positive().nullable()
@@ -2,6 +2,12 @@ import { join } from 'node:path'
2
2
 
3
3
  import { z } from 'zod'
4
4
 
5
+ import {
6
+ DEFAULT_DECISION_MODEL,
7
+ DecisionModelIdSchema,
8
+ type DecisionModelId,
9
+ } from '../jev/jev-schemas'
10
+
5
11
  /**
6
12
  * The engine config file, relative to the root of the repo a run works on.
7
13
  * `muninn.vault` sits at the same path old Luca used, because memory tooling
@@ -33,9 +39,19 @@ export const MuninnConfigSchema = z.object({
33
39
  vault: z.string().min(1),
34
40
  })
35
41
 
42
+ /**
43
+ * The config's `decision_model` block (#534): the Clef model shadow mode
44
+ * asks, `@cf/cloudflare/clef` (the accurate one, the default) or
45
+ * `@cf/cloudflare/clef-flash` (the fast one).
46
+ */
47
+ export const DecisionModelConfigSchema = z.object({
48
+ model: DecisionModelIdSchema.default(DEFAULT_DECISION_MODEL),
49
+ })
50
+
36
51
  /**
37
52
  * The per-repo engine config: the gate commands, where tests live, the rule
38
- * files for the rules lens, the project's memory vault, and the run budget.
53
+ * files for the rules lens, the project's memory vault, the decision model,
54
+ * and the run budget.
39
55
  *
40
56
  * Unknown keys, such as old Luca's, are dropped when the file is read.
41
57
  *
@@ -74,6 +90,8 @@ export const EngineConfigSchema = z.object({
74
90
  test_setup_files: z.array(z.string()).default([]),
75
91
  rule_files: z.array(z.string()).default([]),
76
92
  muninn: MuninnConfigSchema.optional(),
93
+ /** The decision model shadow mode asks; left out, Clef. */
94
+ decision_model: DecisionModelConfigSchema.optional(),
77
95
  /**
78
96
  * The repo's run budget in tokens; left out, the engine's default
79
97
  * (`DEFAULT_RUN_BUDGET_TOKENS`).
@@ -124,6 +142,21 @@ export const prepareConcurrencyOf = ({
124
142
  config: EngineConfig
125
143
  }): number => config.prepare_concurrency ?? DEFAULT_PREPARE_CONCURRENCY
126
144
 
145
+ /**
146
+ * The decision model a run asks: the config's `decision_model.model`, else
147
+ * Clef (`DEFAULT_DECISION_MODEL`).
148
+ *
149
+ * @example
150
+ * decisionModelOf({ config: { ...config, decision_model: { model: '@cf/cloudflare/clef-flash' } } })
151
+ * // '@cf/cloudflare/clef-flash'
152
+ */
153
+ export const decisionModelOf = ({
154
+ config,
155
+ }: {
156
+ config: EngineConfig
157
+ }): DecisionModelId =>
158
+ config.decision_model?.model ?? DecisionModelConfigSchema.parse({}).model
159
+
127
160
  /** One test command the engine runs, and how it reads the results. */
128
161
  export type TestCommand = { run: string; results: TestResults }
129
162