@alecsibilia/luca 14.0.0-alpha.10 → 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 +8 -0
- package/README.md +2 -2
- package/board/client/board-panel.tsx +1 -1
- package/board/package.json +1 -1
- package/board/server/board-vocabulary.ts +9 -1
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +7 -2
- package/board/server/reduce-board.ts +4 -0
- package/board/shared/board-state.ts +3 -2
- package/engine/cli/computer-adapters-real.ts +1 -1
- package/engine/cli/computer-checks.ts +43 -8
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +7 -1
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +6 -0
- package/engine/cli/luca-run.ts +18 -6
- package/engine/cli/luca-skills.ts +49 -25
- package/engine/cli/run-modes.ts +79 -22
- package/engine/cli/upgrade-command.ts +207 -29
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +80 -78
- package/engine/config/config-reload.ts +3 -0
- package/engine/config/engine-config.ts +34 -1
- package/engine/core/execute.ts +29 -24
- package/engine/index.ts +25 -8
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-schemas.ts +43 -4
- package/engine/jev/jev-shadow.ts +95 -63
- package/engine/journal/journal-record.ts +36 -3
- package/engine/journal/replay.ts +4 -1
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/package.json +1 -1
- package/skills/luca-retro/SKILL.md +3 -3
- package/skills/luca-retro/scripts/retro-summary.ts +42 -15
- package/skills/luca-unstick/SKILL.md +1 -1
- package/skills/luca-unstick/scripts/stuck-summary.ts +61 -15
- package/engine/jev/jev-client.ts +0 -173
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ After a run, type `/luca-retro` the same way to look back on it: it reads the ru
|
|
|
49
49
|
| `luca init` | Sets up this computer, once (safe to run again): MuninnDB and its Claude Code entry, the board in Paseo with its engine and Bun paths, the planning skills (`to-spec`, `to-tickets`, `setup-matt-pocock-skills`, `grilling`, `domain-modeling`), and Luca's own `/luca-unstick` and `/luca-retro` skills. It asks before turning on Paseo's plugins, and leaves skills you already have alone (it only replaces its own two). `--skip-skills` skips the planning skills. Inside a git repo, it then runs `luca setup` for that repo. |
|
|
50
50
|
| `luca setup` | Gets the repo in this folder ready: the labels a run needs (and the `release:*` labels when the repo uses changesets), `.luca/config.json` (a new one gets the repo's GitHub name as its vault; an existing vault is kept), and a check of GitHub sub-issues and dependencies. `--base <branch>` names the base branch. |
|
|
51
51
|
| `luca doctor` | Checks this computer, this repo when you're in one, and what v13 left behind. Each problem comes with its exact fix. `luca doctor --fix` fixes what's safe without asking: it never deletes (v13 files go to a dated backup folder) and never commits, and it lists the repo files to commit. |
|
|
52
|
-
| `luca upgrade` | Moves to the newest Luca on your channel: `alpha` stays on `alpha`, and it never goes back to v13. It refuses while any run is going (stuck runs and limit waits count) and lists them. A run that crashed, with no engine running it, doesn't stop it: upgrade says how to resume it on the new version. Then it reloads the board, copies
|
|
52
|
+
| `luca upgrade` | Moves to the newest Luca on your channel: `alpha` stays on `alpha`, and it never goes back to v13. It refuses while any run is going (stuck runs and limit waits count) and lists them. A run that crashed, with no engine running it, doesn't stop it: upgrade says how to resume it on the new version. Then the version it just installed finishes the job with its own code: it reloads the board, copies its skills (every one it ships, new ones included), and says when to run `/reload-skills`. |
|
|
53
53
|
| `luca upgrade --to <version>` | Installs that exact version, an older one too, to go back when a version breaks. |
|
|
54
54
|
|
|
55
55
|
`init`, `setup`, and `upgrade` end with their part of `luca doctor`'s checks.
|
|
@@ -92,7 +92,7 @@ This is the one package that goes to npm. It's TypeScript source with no build a
|
|
|
92
92
|
| ------ | ---------- |
|
|
93
93
|
| `engine/` | The engine's source, from [`packages/engine/src`](../engine/src). Both bins point into it. |
|
|
94
94
|
| `board/` | The board plugin's folder, from [`packages/board`](../board): `paseo-plugin.json`, its entry points, its client, server, and shared code, and its own `package.json`, so Paseo can install it as a folder source. |
|
|
95
|
-
| `skills/` | Luca's own Claude Code skills, `luca-unstick` and `luca-retro`, from [`packages/engine/skills`](../engine/skills). `luca init` and `luca upgrade` copy
|
|
95
|
+
| `skills/` | Luca's own Claude Code skills, `luca-unstick` and `luca-retro`, from [`packages/engine/skills`](../engine/skills). `luca init` and `luca upgrade` copy every folder in it with a `SKILL.md` to `~/.claude/skills`. |
|
|
96
96
|
| `LICENSE` | The repo's license. |
|
|
97
97
|
|
|
98
98
|
Test files and test helpers stay out. The engine and the board stay private workspace packages, and this package lists every package they import at runtime as its own dependency.
|
|
@@ -1327,7 +1327,7 @@ export const BoardPanel = ({
|
|
|
1327
1327
|
) : null}
|
|
1328
1328
|
{state.jev.asked > 0 ? (
|
|
1329
1329
|
<Text style={styles.small}>
|
|
1330
|
-
|
|
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>
|
package/board/package.json
CHANGED
|
@@ -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:
|
|
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.
|
|
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,
|
|
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
|
|
486
|
-
* engine
|
|
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),
|
|
@@ -400,7 +400,7 @@ const PackageJsonSchema = z.looseObject({
|
|
|
400
400
|
})
|
|
401
401
|
|
|
402
402
|
/** The package a file belongs to: the nearest package.json with a name. */
|
|
403
|
-
const packageOf = async (
|
|
403
|
+
export const packageOf = async (
|
|
404
404
|
file: string
|
|
405
405
|
): Promise<{ name: string; version: string | null } | null> => {
|
|
406
406
|
for (let dir = dirname(file); ; dir = dirname(dir)) {
|
|
@@ -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,
|
|
36
|
-
*
|
|
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. */
|
|
@@ -329,8 +335,8 @@ const checkPlanningSkills = async ({ home }: { home: string }) => {
|
|
|
329
335
|
}
|
|
330
336
|
|
|
331
337
|
/**
|
|
332
|
-
* Luca's own skills (#504): each
|
|
333
|
-
* are the ones
|
|
338
|
+
* Luca's own skills (#504): each one in the installed Luca's skills folder
|
|
339
|
+
* (#529) is in `~/.claude/skills`, and its files are the ones it ships.
|
|
334
340
|
*/
|
|
335
341
|
const checkLucaSkills = async ({
|
|
336
342
|
home,
|
|
@@ -342,10 +348,9 @@ const checkLucaSkills = async ({
|
|
|
342
348
|
luca_version: string
|
|
343
349
|
}) => {
|
|
344
350
|
const drift = await skillDrift({ home, skills_dir })
|
|
345
|
-
|
|
346
|
-
if (unshipped.length > 0) {
|
|
351
|
+
if (drift.length === 0) {
|
|
347
352
|
return problem({
|
|
348
|
-
detail: `Luca's folder ${skills_dir} has no
|
|
353
|
+
detail: `Luca's folder ${skills_dir} has no skills.`,
|
|
349
354
|
fix: `Reinstall Luca: bun add -g ${LUCA_PACKAGE}@alpha`,
|
|
350
355
|
})
|
|
351
356
|
}
|
|
@@ -368,12 +373,32 @@ const checkLucaSkills = async ({
|
|
|
368
373
|
)
|
|
369
374
|
}
|
|
370
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
|
+
|
|
371
396
|
/**
|
|
372
397
|
* Runs the computer checks, in order, read-only. Never throws: a check that
|
|
373
398
|
* fails becomes a problem.
|
|
374
399
|
*
|
|
375
400
|
* @example
|
|
376
|
-
* 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 })
|
|
377
402
|
*/
|
|
378
403
|
export const computerChecks = async ({
|
|
379
404
|
home,
|
|
@@ -387,6 +412,7 @@ export const computerChecks = async ({
|
|
|
387
412
|
muninn_health,
|
|
388
413
|
claude,
|
|
389
414
|
paseo,
|
|
415
|
+
env,
|
|
390
416
|
}: {
|
|
391
417
|
home: string
|
|
392
418
|
/** The installed Luca's version. */
|
|
@@ -401,6 +427,11 @@ export const computerChecks = async ({
|
|
|
401
427
|
muninn_health: MuninnHealth
|
|
402
428
|
claude: ClaudeMcp
|
|
403
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
|
|
404
435
|
}): Promise<DoctorCheck[]> => {
|
|
405
436
|
// With Paseo down, its plugins and the board aren't asked about.
|
|
406
437
|
let paseo_up = true
|
|
@@ -447,6 +478,10 @@ export const computerChecks = async ({
|
|
|
447
478
|
'luca_skills',
|
|
448
479
|
() => checkLucaSkills({ home, skills_dir, luca_version }),
|
|
449
480
|
],
|
|
481
|
+
[
|
|
482
|
+
'decision_model',
|
|
483
|
+
() => checkDecisionModel({ env: env ?? {}, home }),
|
|
484
|
+
],
|
|
450
485
|
],
|
|
451
486
|
})
|
|
452
487
|
}
|
package/engine/cli/doctor.ts
CHANGED
|
@@ -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)
|
|
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
|
? []
|
package/engine/cli/init.ts
CHANGED
|
@@ -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 =
|
package/engine/cli/luca-run.ts
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
152
|
+
decision_model: await decisionModelSetup(),
|
|
141
153
|
memory: await memoryOf(),
|
|
142
154
|
usage: usageLine(),
|
|
143
155
|
board,
|
|
@@ -5,20 +5,22 @@ import type { StepEnd } from './board-in-paseo'
|
|
|
5
5
|
import { reason } from './doctor-checks'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Luca's own Claude Code skills
|
|
9
|
-
* (#514). They ship in the package's `skills/` folder, next
|
|
10
|
-
* and `board/` (`packages/engine/skills/` in the repo). `luca
|
|
11
|
-
* copy them to `~/.claude/skills/<skill>/`, so
|
|
12
|
-
* any Paseo chat, and `luca doctor` checks the
|
|
13
|
-
* installed Luca ships (`luca doctor --fix` copies it
|
|
8
|
+
* Luca's own Claude Code skills, such as `/luca-unstick` (#504) and
|
|
9
|
+
* `/luca-retro` (#514). They ship in the package's `skills/` folder, next
|
|
10
|
+
* to `engine/` and `board/` (`packages/engine/skills/` in the repo). `luca
|
|
11
|
+
* init` and `luca upgrade` copy them to `~/.claude/skills/<skill>/`, so
|
|
12
|
+
* they work in any repo and any Paseo chat, and `luca doctor` checks the
|
|
13
|
+
* copy is the one the installed Luca ships (`luca doctor --fix` copies it
|
|
14
|
+
* again).
|
|
15
|
+
*
|
|
16
|
+
* Which skills there are comes from that folder, not a list in the code
|
|
17
|
+
* (#529): every folder in it with a `SKILL.md`. So a skill new in a
|
|
18
|
+
* release is copied and checked even by code from before it.
|
|
14
19
|
*
|
|
15
20
|
* A copy writes every file the skill ships (not its tests), over Luca's own
|
|
16
21
|
* earlier copy. It never deletes, and never touches other skills.
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
|
-
/** Luca's own skills, by folder name. */
|
|
20
|
-
export const LUCA_SKILLS = ['luca-unstick', 'luca-retro']
|
|
21
|
-
|
|
22
24
|
/** Test files stay in the repo: they aren't part of a skill. */
|
|
23
25
|
const isTestFile = (path: string) => /\.test\.[cm]?[jt]sx?$/.test(path)
|
|
24
26
|
|
|
@@ -26,6 +28,31 @@ const isTestFile = (path: string) => /\.test\.[cm]?[jt]sx?$/.test(path)
|
|
|
26
28
|
export const claudeSkillsDir = ({ home }: { home: string }): string =>
|
|
27
29
|
join(home, '.claude', 'skills')
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Luca's own skills in `skills_dir`, by folder name, sorted: each folder
|
|
33
|
+
* with a `SKILL.md`. None when the folder is missing.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* await lucaSkills({ skills_dir }) // ['luca-retro', 'luca-unstick']
|
|
37
|
+
*/
|
|
38
|
+
export const lucaSkills = async ({
|
|
39
|
+
skills_dir,
|
|
40
|
+
}: {
|
|
41
|
+
skills_dir: string
|
|
42
|
+
}): Promise<string[]> => {
|
|
43
|
+
try {
|
|
44
|
+
const manifests = await Array.fromAsync(
|
|
45
|
+
new Bun.Glob('*/SKILL.md').scan({
|
|
46
|
+
cwd: skills_dir,
|
|
47
|
+
onlyFiles: true,
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
return manifests.map((manifest) => dirname(manifest)).toSorted()
|
|
51
|
+
} catch {
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* The files one skill ships, relative to its folder in `skills_dir`,
|
|
31
58
|
* sorted: every file but tests. None when the folder is missing.
|
|
@@ -57,8 +84,6 @@ export const skillFiles = async ({
|
|
|
57
84
|
/** How one installed skill differs from the one Luca ships. */
|
|
58
85
|
export type SkillDrift = {
|
|
59
86
|
skill: string
|
|
60
|
-
/** Luca's install folder has no files for it. */
|
|
61
|
-
unshipped: boolean
|
|
62
87
|
/** Shipped files the installed copy lacks. */
|
|
63
88
|
missing: string[]
|
|
64
89
|
/** Shipped files whose installed copy has other content. */
|
|
@@ -67,7 +92,8 @@ export type SkillDrift = {
|
|
|
67
92
|
|
|
68
93
|
/**
|
|
69
94
|
* How each of Luca's skills in `~/.claude/skills` differs from the ones in
|
|
70
|
-
* `skills_dir`; a skill that matches has empty lists.
|
|
95
|
+
* `skills_dir`; a skill that matches has empty lists. None when
|
|
96
|
+
* `skills_dir` has no skills. Read-only.
|
|
71
97
|
*/
|
|
72
98
|
export const skillDrift = async ({
|
|
73
99
|
home,
|
|
@@ -77,7 +103,7 @@ export const skillDrift = async ({
|
|
|
77
103
|
skills_dir: string
|
|
78
104
|
}): Promise<SkillDrift[]> => {
|
|
79
105
|
const drift: SkillDrift[] = []
|
|
80
|
-
for (const skill of
|
|
106
|
+
for (const skill of await lucaSkills({ skills_dir })) {
|
|
81
107
|
const files = await skillFiles({ skills_dir, skill })
|
|
82
108
|
const missing: string[] = []
|
|
83
109
|
const changed: string[] = []
|
|
@@ -94,7 +120,7 @@ export const skillDrift = async ({
|
|
|
94
120
|
changed.push(file)
|
|
95
121
|
}
|
|
96
122
|
}
|
|
97
|
-
drift.push({ skill,
|
|
123
|
+
drift.push({ skill, missing, changed })
|
|
98
124
|
}
|
|
99
125
|
return drift
|
|
100
126
|
}
|
|
@@ -107,7 +133,7 @@ export const skillDrift = async ({
|
|
|
107
133
|
*
|
|
108
134
|
* @example
|
|
109
135
|
* await installLucaSkills({ home: homedir(), skills_dir, prefix: '[luca init]', log: console.log })
|
|
110
|
-
* // [luca init] Luca's skills: installed /luca-
|
|
136
|
+
* // [luca init] Luca's skills: installed /luca-retro, /luca-unstick in ~/.claude/skills
|
|
111
137
|
*/
|
|
112
138
|
export const installLucaSkills = async ({
|
|
113
139
|
home,
|
|
@@ -125,17 +151,15 @@ export const installLucaSkills = async ({
|
|
|
125
151
|
return { ok, message }
|
|
126
152
|
}
|
|
127
153
|
try {
|
|
154
|
+
const drift = await skillDrift({ home, skills_dir })
|
|
155
|
+
if (drift.length === 0) {
|
|
156
|
+
return say({
|
|
157
|
+
message: `${prefix} Luca's skills: Luca's folder ${skills_dir} has none. Reinstall Luca.`,
|
|
158
|
+
ok: false,
|
|
159
|
+
})
|
|
160
|
+
}
|
|
128
161
|
const written: string[] = []
|
|
129
|
-
for (const { skill,
|
|
130
|
-
home,
|
|
131
|
-
skills_dir,
|
|
132
|
-
})) {
|
|
133
|
-
if (unshipped) {
|
|
134
|
-
return say({
|
|
135
|
-
message: `${prefix} Luca's skills: ${skill} isn't in Luca's folder ${skills_dir}. Reinstall Luca.`,
|
|
136
|
-
ok: false,
|
|
137
|
-
})
|
|
138
|
-
}
|
|
162
|
+
for (const { skill, missing, changed } of drift) {
|
|
139
163
|
for (const file of [...missing, ...changed]) {
|
|
140
164
|
const target = join(claudeSkillsDir({ home }), skill, file)
|
|
141
165
|
await mkdir(dirname(target), { recursive: true })
|