@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.
- package/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A turn's live progress (#628), for the engine's live file: what the
|
|
3
|
+
* agent and its helpers (#636) are doing now, read from the SDK messages (streamed chunks too,
|
|
4
|
+
* #631) and the guard hook. It
|
|
5
|
+
* holds only short words: a tool and a target (a path relative to the
|
|
6
|
+
* worktree, or the first words of a command), never a prompt, a file's
|
|
7
|
+
* contents or a command's output.
|
|
8
|
+
*/
|
|
9
|
+
import { basename, isAbsolute, join, relative } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
|
|
13
|
+
import { RateLimitSchema, toolResultIds } from './claude-messages'
|
|
14
|
+
|
|
15
|
+
import { HELPER_MODEL, HELPER_TOOL } from '../guards/helper-rules'
|
|
16
|
+
|
|
17
|
+
/** The agent's latest action: a tool and a short target, with its time (ISO). */
|
|
18
|
+
export type ProgressAction = { tool: string; target: string; at: string }
|
|
19
|
+
|
|
20
|
+
/** A tool call the guard let through whose result hasn't come back yet. */
|
|
21
|
+
export type ProgressOpenCall = {
|
|
22
|
+
tool: string
|
|
23
|
+
target: string
|
|
24
|
+
started_at: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A helper the agent started this turn (#636): its model, its latest
|
|
29
|
+
* action, and whether its result has come back.
|
|
30
|
+
*/
|
|
31
|
+
export type ProgressHelper = {
|
|
32
|
+
model: string
|
|
33
|
+
action: ProgressAction | null
|
|
34
|
+
done: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What one turn has done so far, as `on_progress` hears it. */
|
|
38
|
+
export type TurnProgress = {
|
|
39
|
+
/** Every token the turn's messages read or wrote so far, cache reads included. */
|
|
40
|
+
tokens: number
|
|
41
|
+
action: ProgressAction | null
|
|
42
|
+
open_call: ProgressOpenCall | null
|
|
43
|
+
/** The turn's helpers, oldest first; none when left out. */
|
|
44
|
+
helpers?: ProgressHelper[]
|
|
45
|
+
/** The turn's `rate_limit_event` readings so far, each with `arrived_at`. */
|
|
46
|
+
rate_limit_events: Record<string, unknown>[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Hears a turn's progress each time it changes. */
|
|
50
|
+
export type ProgressListener = (progress: TurnProgress) => void
|
|
51
|
+
|
|
52
|
+
/** How many words of a command a target keeps. */
|
|
53
|
+
const COMMAND_WORDS = 3
|
|
54
|
+
|
|
55
|
+
/** The longest target, in characters. */
|
|
56
|
+
const MAX_TARGET = 60
|
|
57
|
+
|
|
58
|
+
const UsageSchema = z.object({
|
|
59
|
+
input_tokens: z.number().default(0),
|
|
60
|
+
output_tokens: z.number().default(0),
|
|
61
|
+
cache_read_input_tokens: z.number().nullable().default(0),
|
|
62
|
+
cache_creation_input_tokens: z.number().nullable().default(0),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const AssistantSchema = z.object({
|
|
66
|
+
type: z.literal('assistant'),
|
|
67
|
+
uuid: z.string().optional(),
|
|
68
|
+
parent_tool_use_id: z.string().nullable().default(null),
|
|
69
|
+
message: z.object({
|
|
70
|
+
id: z.string().optional(),
|
|
71
|
+
content: z.array(z.unknown()).default([]),
|
|
72
|
+
usage: UsageSchema.optional(),
|
|
73
|
+
}),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const ToolUseSchema = z.object({
|
|
77
|
+
type: z.literal('tool_use'),
|
|
78
|
+
name: z.string(),
|
|
79
|
+
input: z.unknown(),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
/** The parts of a tool call's input a target is made from. */
|
|
83
|
+
const TargetInputSchema = z
|
|
84
|
+
.object({
|
|
85
|
+
file_path: z.unknown(),
|
|
86
|
+
notebook_path: z.unknown(),
|
|
87
|
+
command: z.unknown(),
|
|
88
|
+
pattern: z.unknown(),
|
|
89
|
+
subagent_type: z.unknown(),
|
|
90
|
+
})
|
|
91
|
+
.partial()
|
|
92
|
+
|
|
93
|
+
/** A streamed chunk of the model's answer (#631), with partial messages on. */
|
|
94
|
+
const StreamEventSchema = z.object({ type: z.literal('stream_event') })
|
|
95
|
+
|
|
96
|
+
const textOf = (value: unknown): string | null =>
|
|
97
|
+
typeof value === 'string' && value.trim() !== '' ? value.trim() : null
|
|
98
|
+
|
|
99
|
+
/** A path relative to the worktree; one outside it keeps its name only. */
|
|
100
|
+
const shortPath = ({
|
|
101
|
+
path,
|
|
102
|
+
worktree,
|
|
103
|
+
}: {
|
|
104
|
+
path: string
|
|
105
|
+
worktree: string
|
|
106
|
+
}): string => {
|
|
107
|
+
const inside = relative(
|
|
108
|
+
worktree,
|
|
109
|
+
isAbsolute(path) ? path : join(worktree, path)
|
|
110
|
+
)
|
|
111
|
+
return inside.startsWith('..') || isAbsolute(inside)
|
|
112
|
+
? basename(path)
|
|
113
|
+
: inside
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The first words of a command's first line. */
|
|
117
|
+
const firstWords = (command: string): string =>
|
|
118
|
+
(command.split('\n')[0] ?? '')
|
|
119
|
+
.trim()
|
|
120
|
+
.match(new RegExp(`^\\S+(?:\\s+\\S+){0,${COMMAND_WORDS - 1}}`))?.[0] ??
|
|
121
|
+
''
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A tool call's short target: the path it works on, the first words of
|
|
125
|
+
* its command, its search pattern, or its helper's type.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* targetOf({ tool_name: 'Bash', tool_input: { command: 'bun test src --bail' }, worktree }) // 'bun test src'
|
|
129
|
+
*/
|
|
130
|
+
export const targetOf = ({
|
|
131
|
+
tool_name,
|
|
132
|
+
tool_input,
|
|
133
|
+
worktree,
|
|
134
|
+
}: {
|
|
135
|
+
tool_name: string
|
|
136
|
+
tool_input: unknown
|
|
137
|
+
worktree: string
|
|
138
|
+
}): string => {
|
|
139
|
+
const parsed = TargetInputSchema.safeParse(tool_input)
|
|
140
|
+
const input = parsed.success ? parsed.data : {}
|
|
141
|
+
const path = textOf(input.file_path) ?? textOf(input.notebook_path)
|
|
142
|
+
const target =
|
|
143
|
+
path !== null
|
|
144
|
+
? shortPath({ path, worktree })
|
|
145
|
+
: tool_name === 'Bash'
|
|
146
|
+
? firstWords(textOf(input.command) ?? '')
|
|
147
|
+
: (textOf(input.pattern) ?? textOf(input.subagent_type) ?? '')
|
|
148
|
+
return target.slice(0, MAX_TARGET)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Keeps one session's turn progress and tells `on_progress` each time it
|
|
153
|
+
* changes. `start` begins a turn afresh, with its own listener;
|
|
154
|
+
* `readings` gives the turn's plan-usage readings, as the session's
|
|
155
|
+
* summary has them.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* const progress = createTurnProgress({ worktree, readings: () => summary.rate_limit_events })
|
|
159
|
+
* progress.start(on_progress)
|
|
160
|
+
* progress.heard(message)
|
|
161
|
+
*/
|
|
162
|
+
export const createTurnProgress = ({
|
|
163
|
+
worktree,
|
|
164
|
+
readings,
|
|
165
|
+
}: {
|
|
166
|
+
worktree: string
|
|
167
|
+
readings: () => Record<string, unknown>[]
|
|
168
|
+
}) => {
|
|
169
|
+
let listener: ProgressListener | undefined
|
|
170
|
+
let tokens = new Map<string, number>()
|
|
171
|
+
let action: ProgressAction | null = null
|
|
172
|
+
let open = new Map<string, ProgressOpenCall>()
|
|
173
|
+
/** The turn's helpers, by the `Agent` call that started each. */
|
|
174
|
+
let helpers = new Map<string, ProgressHelper>()
|
|
175
|
+
|
|
176
|
+
const tell = (): void => {
|
|
177
|
+
listener?.({
|
|
178
|
+
tokens: [...tokens.values()].reduce((sum, each) => sum + each, 0),
|
|
179
|
+
action,
|
|
180
|
+
open_call: [...open.values()].at(-1) ?? null,
|
|
181
|
+
helpers: [...helpers.values()].map((helper) => ({ ...helper })),
|
|
182
|
+
rate_limit_events: [...readings()],
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const start = (on_progress: ProgressListener | undefined): void => {
|
|
187
|
+
listener = on_progress
|
|
188
|
+
tokens = new Map()
|
|
189
|
+
action = null
|
|
190
|
+
open = new Map()
|
|
191
|
+
helpers = new Map()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Reads one SDK message: its tokens, its tool calls, the results back. A
|
|
196
|
+
* streamed chunk is told as it comes, so the heartbeat moves while the
|
|
197
|
+
* model thinks, but nothing of it is kept: the whole message that
|
|
198
|
+
* follows carries the tokens.
|
|
199
|
+
*/
|
|
200
|
+
const heard = (raw: unknown): void => {
|
|
201
|
+
if (
|
|
202
|
+
RateLimitSchema.safeParse(raw).success ||
|
|
203
|
+
StreamEventSchema.safeParse(raw).success
|
|
204
|
+
) {
|
|
205
|
+
tell()
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
const assistant = AssistantSchema.safeParse(raw)
|
|
209
|
+
if (assistant.success) {
|
|
210
|
+
const { message, uuid, parent_tool_use_id } = assistant.data
|
|
211
|
+
const usage = message.usage
|
|
212
|
+
if (usage !== undefined) {
|
|
213
|
+
tokens.set(
|
|
214
|
+
message.id ?? uuid ?? String(tokens.size),
|
|
215
|
+
usage.input_tokens +
|
|
216
|
+
usage.output_tokens +
|
|
217
|
+
(usage.cache_read_input_tokens ?? 0) +
|
|
218
|
+
(usage.cache_creation_input_tokens ?? 0)
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
// A helper's own calls are its action, not its agent's.
|
|
222
|
+
const helper =
|
|
223
|
+
parent_tool_use_id === null
|
|
224
|
+
? undefined
|
|
225
|
+
: helpers.get(parent_tool_use_id)
|
|
226
|
+
for (const block of message.content) {
|
|
227
|
+
const call = ToolUseSchema.safeParse(block)
|
|
228
|
+
if (!call.success) continue
|
|
229
|
+
const latest = {
|
|
230
|
+
tool: call.data.name,
|
|
231
|
+
target: targetOf({
|
|
232
|
+
tool_name: call.data.name,
|
|
233
|
+
tool_input: call.data.input,
|
|
234
|
+
worktree,
|
|
235
|
+
}),
|
|
236
|
+
at: new Date().toISOString(),
|
|
237
|
+
}
|
|
238
|
+
if (parent_tool_use_id === null) action = latest
|
|
239
|
+
else if (helper !== undefined) helper.action = latest
|
|
240
|
+
}
|
|
241
|
+
tell()
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
const ids = toolResultIds(raw)
|
|
245
|
+
if (!ids.some((id) => open.has(id) || helpers.has(id))) return
|
|
246
|
+
for (const id of ids) {
|
|
247
|
+
open.delete(id)
|
|
248
|
+
const helper = helpers.get(id)
|
|
249
|
+
if (helper !== undefined) helper.done = true
|
|
250
|
+
}
|
|
251
|
+
tell()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A tool call the guard let through: open until its result comes back. */
|
|
255
|
+
const opened = ({
|
|
256
|
+
tool_name,
|
|
257
|
+
tool_input,
|
|
258
|
+
tool_use_id,
|
|
259
|
+
}: {
|
|
260
|
+
tool_name: string
|
|
261
|
+
tool_input: unknown
|
|
262
|
+
tool_use_id: string
|
|
263
|
+
}): void => {
|
|
264
|
+
open.set(tool_use_id, {
|
|
265
|
+
tool: tool_name,
|
|
266
|
+
target: targetOf({ tool_name, tool_input, worktree }),
|
|
267
|
+
started_at: new Date().toISOString(),
|
|
268
|
+
})
|
|
269
|
+
if (tool_name === HELPER_TOOL) {
|
|
270
|
+
helpers.set(tool_use_id, {
|
|
271
|
+
model: HELPER_MODEL,
|
|
272
|
+
action: null,
|
|
273
|
+
done: false,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
tell()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { start, heard, opened }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The progress `createTurnProgress` keeps for one session. */
|
|
283
|
+
export type TurnProgressKeeper = ReturnType<typeof createTurnProgress>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task sections of the roles that write: the test-writer and the
|
|
3
|
+
* implementer.
|
|
4
|
+
*/
|
|
5
|
+
import { engineChores, REVIEW_FIXER, setupFiles } from './common-instructions'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
caseLimitOf,
|
|
9
|
+
featureMapPath,
|
|
10
|
+
isFeatureMapFolder,
|
|
11
|
+
slowTestCommands,
|
|
12
|
+
testCommands,
|
|
13
|
+
type EngineConfig,
|
|
14
|
+
} from '../config/engine-config'
|
|
15
|
+
import { MAX_TESTS_PER_CRITERION } from '../core/loop-caps'
|
|
16
|
+
|
|
17
|
+
const TEST_UPDATE = `## Updating tests after a rebase
|
|
18
|
+
Sometimes other tickets joined the run branch after this ticket's tests were written, and the engine moved this ticket's change on top of it. If the implementer then says one of your tests is wrong, the tests come back to you. Your prompt says what joined, and the test and the implementer's reason. Then:
|
|
19
|
+
- The ticket's code is already in the worktree. Never touch it; edit test files only.
|
|
20
|
+
- Change a test only where the run branch's behavior changed what it should expect. Read what joined with \`git log\` and \`git show\`. Don't weaken a test just to make it pass.
|
|
21
|
+
- Every criterion still needs a test. Your updated tests may pass now, since the code is there; that is fine. The engine runs every test in the gates after the implementer's turn.
|
|
22
|
+
- Answer "tests_written" with the full criterion mapping again.`
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* When a writer may answer `environment` (#613), and that the engine checks
|
|
26
|
+
* the claim by rerunning the failed check.
|
|
27
|
+
*/
|
|
28
|
+
const environmentLine = ({ check }: { check: string }): string =>
|
|
29
|
+
`- If ${check} failed for a reason that is not this ticket's fault (the machine or another ticket: a port already in use, EADDRINUSE; an old test that also fails at baseline), don't fake a fix: answer outcome "environment", with your evidence in "environment": { evidence } (the error lines, and what you checked). The engine checks the claim by rerunning the failed check once, with no fix round used. If the rerun fails the same way, the ticket is stuck for the owner, so answer it only when you are sure.`
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The test-writer's lines about the feature map (#583), or nothing without
|
|
33
|
+
* one. A folder map (#597) is read page by page, and a new test file goes on
|
|
34
|
+
* its area's page.
|
|
35
|
+
*/
|
|
36
|
+
const featureMapLines = ({ config }: { config: EngineConfig }): string => {
|
|
37
|
+
const path = featureMapPath({ config })
|
|
38
|
+
if (path === null) return ''
|
|
39
|
+
return isFeatureMapFolder({ path })
|
|
40
|
+
? `- Read the feature map's pages (the *.md files under ${path}/) first: they list each feature and the test files that prove it.\n- Put new tests in a feature's existing test files when you can. A new test file must be listed under its feature on its area's page in ${path}/; you may edit or add those pages.\n`
|
|
41
|
+
: `- Read ${path} first: it lists each feature and the test files that prove it.\n- Put new tests in a feature's existing test files when you can. A new test file must be listed under its feature in ${path}; you may edit that file.\n`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What the test-writer's "ONLY test files" line adds for the feature map, or nothing. */
|
|
45
|
+
const featureMapClause = ({ config }: { config: EngineConfig }): string => {
|
|
46
|
+
const path = featureMapPath({ config })
|
|
47
|
+
if (path === null) return ''
|
|
48
|
+
return isFeatureMapFolder({ path })
|
|
49
|
+
? ` and the feature map's pages (*.md under ${path}/)`
|
|
50
|
+
: ` and the feature map ${path}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What the test-writer's "ONLY test files" line adds for test helpers (#619), or nothing. */
|
|
54
|
+
const testHelperClause = ({ config }: { config: EngineConfig }): string => {
|
|
55
|
+
const patterns = config.test_helper_patterns ?? []
|
|
56
|
+
return patterns.length === 0
|
|
57
|
+
? ''
|
|
58
|
+
: `, test helpers only tests use, such as fakes and fixtures (${patterns.join(', ')})`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const TEST_WRITER = ({
|
|
62
|
+
config,
|
|
63
|
+
}: {
|
|
64
|
+
config: EngineConfig
|
|
65
|
+
}) => `# Your role: test-writer
|
|
66
|
+
|
|
67
|
+
You write the failing tests for ONE ticket, before any code exists.
|
|
68
|
+
|
|
69
|
+
- Create or edit ONLY test files (${config.test_file_patterns.join(', ')})${testHelperClause({ config })}${featureMapClause({ config })}. No other file: no implementation, no package.json, no config, no notes, and not the test setup files.
|
|
70
|
+
- Your new tests must FAIL right now, because the code does not exist yet. That is expected. Do not create or stub the module under test.
|
|
71
|
+
- Test only the public behavior the ticket names, at the seams the spec's "Testing Decisions" section names. No mocks unless the spec asks. No tautological asserts.
|
|
72
|
+
${featureMapLines({ config })}- Every acceptance criterion needs at least one test, and at most ${MAX_TESTS_PER_CRITERION}: write the fewest tests that prove it. Old tests must keep passing.
|
|
73
|
+
- Map every test you add. The red check refuses a new test your criterion mapping doesn't name.
|
|
74
|
+
- Give every test a plain string-literal name (no test.each, no template names), so the engine can find each one before the module exists.
|
|
75
|
+
|
|
76
|
+
Your result (structured output):
|
|
77
|
+
- outcome: "tests_written", or "nothing_new_to_test" if the ticket truly changes no behavior (a refactor), or "already_done" if the ticket's work is already on the base branch, or "needs_setup_change" if the tests can't work without a change to a test setup file${setupFiles({ config })} (you may never change one): say which file and why in "setup_change", and the user makes the change.
|
|
78
|
+
- Answer "already_done" only when EVERY acceptance criterion is already met on the base branch and already has a test that checks it. Look with \`git log\` and \`git show\`, and read the code and tests. Then write no tests, and give your evidence: in "done_by", the commits that did the work, as { sha, title }; in "criteria", the existing tests that cover each criterion. The engine checks this evidence: each commit must be on the base branch, and each test must exist and pass there. If even one criterion is not met, or not tested, write the tests instead.
|
|
79
|
+
- criteria: for EACH criterion id (AC1, AC2, ...), the tests that check it, as { file, name }. "name" is the full name bun prints: describe names and the test name joined by " > ".
|
|
80
|
+
${environmentLine({ check: 'the red check' })}
|
|
81
|
+
- done_by: empty, unless the outcome is "already_done".
|
|
82
|
+
- finding_responses: empty, unless your prompt gives you ticket review findings (see below).
|
|
83
|
+
- file_modes, run_generators: empty, unless you need a chore below.
|
|
84
|
+
- summary, assumptions, run_notes.
|
|
85
|
+
|
|
86
|
+
${engineChores({ config })}
|
|
87
|
+
|
|
88
|
+
${TEST_UPDATE}
|
|
89
|
+
|
|
90
|
+
${REVIEW_FIXER}`
|
|
91
|
+
|
|
92
|
+
const testFileRule = ({
|
|
93
|
+
config,
|
|
94
|
+
may_edit_tests,
|
|
95
|
+
}: {
|
|
96
|
+
config: EngineConfig
|
|
97
|
+
may_edit_tests: boolean
|
|
98
|
+
}): string =>
|
|
99
|
+
may_edit_tests
|
|
100
|
+
? `- You may follow renames and moves into test files (${config.test_file_patterns.join(', ')}), but never change what a test checks. Never touch a test setup file${setupFiles({ config })}.`
|
|
101
|
+
: `- Never create, edit, or delete a test file (${config.test_file_patterns.join(', ')}) or a test setup file${setupFiles({ config })}.`
|
|
102
|
+
|
|
103
|
+
/** The implementer's line about the slow tests (#563), or nothing without them. */
|
|
104
|
+
const slowLine = ({ config }: { config: EngineConfig }): string => {
|
|
105
|
+
const slow = slowTestCommands({ config }).map(({ run }) => `\`${run}\``)
|
|
106
|
+
return slow.length === 0
|
|
107
|
+
? ''
|
|
108
|
+
: ` The slow tests (${slow.join(', ')}) run at the final review, and the ticket's gates also run the slow test files it added or changed; you may run them yourself.`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The implementer's line about the fast test limit (#580), or nothing without
|
|
113
|
+
* one.
|
|
114
|
+
*/
|
|
115
|
+
const limitLine = ({ config }: { config: EngineConfig }): string => {
|
|
116
|
+
const limit = caseLimitOf({ config })
|
|
117
|
+
return limit === null
|
|
118
|
+
? ''
|
|
119
|
+
: `- Each fast test must finish within ${limit} ms. If one of the ticket's tests can't, answer "bad_test".\n`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const IMPLEMENTER = ({
|
|
123
|
+
config,
|
|
124
|
+
may_edit_tests,
|
|
125
|
+
}: {
|
|
126
|
+
config: EngineConfig
|
|
127
|
+
may_edit_tests: boolean
|
|
128
|
+
}) => `# Your role: implementer
|
|
129
|
+
|
|
130
|
+
You write the code that makes ONE ticket's failing tests pass. Another agent wrote the tests; the engine already committed them.
|
|
131
|
+
|
|
132
|
+
- Make every gate pass: ${[
|
|
133
|
+
...testCommands({ config }).map(({ run }) => run),
|
|
134
|
+
config.checks.types,
|
|
135
|
+
config.checks.lint,
|
|
136
|
+
]
|
|
137
|
+
.filter((command) => command !== undefined)
|
|
138
|
+
.map((command) => `\`${command}\``)
|
|
139
|
+
.join(', ')}.${slowLine({ config })}
|
|
140
|
+
${limitLine({ config })}${testFileRule({ config, may_edit_tests })}
|
|
141
|
+
- If a test is wrong (it contradicts the ticket or spec, or no correct code can pass it), don't work around it: answer outcome "bad_test" with the test and your reason.
|
|
142
|
+
- If the gates can't pass without a change to a test setup file${setupFiles({ config })}, don't work around it: answer outcome "needs_setup_change", with the file and why in "setup_change". The user makes the change.
|
|
143
|
+
${environmentLine({ check: 'a gate' })}
|
|
144
|
+
- Follow the spec's Implementation Decisions. Keep the change small: no extra features, and no new dependencies unless the spec says so.
|
|
145
|
+
- Leave nothing behind: no scratch files, logs, notes, or unused scripts. The engine scans for leftovers before it commits.
|
|
146
|
+
|
|
147
|
+
Your result (structured output): outcome ("done", "bad_test", "needs_setup_change", or "environment"), bad_test (null unless outcome is "bad_test"), setup_change (null unless outcome is "needs_setup_change"), environment (null unless outcome is "environment"), finding_responses (empty unless you were sent ticket review findings), file_modes and run_generators (empty unless you need a chore below), summary, assumptions, run_notes.
|
|
148
|
+
|
|
149
|
+
${engineChores({ config })}
|
|
150
|
+
|
|
151
|
+
${REVIEW_FIXER}`
|
|
@@ -372,12 +372,24 @@ export const boardDir = async (): Promise<string> => {
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
/**
|
|
375
|
-
* The
|
|
376
|
-
* the
|
|
375
|
+
* The skills folder in Luca's install folder: `skills/` next to `engine/`
|
|
376
|
+
* in the published package, or `packages/engine/skills` in a working copy
|
|
377
|
+
* (both two folders up from this file). Its real path when it exists;
|
|
378
|
+
* `luca doctor` reports it when it doesn't.
|
|
379
|
+
*/
|
|
380
|
+
export const skillsDir = async (): Promise<string> => {
|
|
381
|
+
const dir = join(import.meta.dir, '..', '..', 'skills')
|
|
382
|
+
return realpath(dir).catch(() => dir)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The installed Luca: its version (see `lucaVersion`), its board and skills
|
|
387
|
+
* folders, the real path of its `luca-run`, and Bun's own path.
|
|
377
388
|
*/
|
|
378
389
|
export const lucaInstall = async (): Promise<LucaInstall> => ({
|
|
379
390
|
luca_version: lucaVersion(),
|
|
380
391
|
board_dir: await boardDir(),
|
|
392
|
+
skills_dir: await skillsDir(),
|
|
381
393
|
engine_path: await realpath(join(import.meta.dir, 'luca-run.ts')),
|
|
382
394
|
bun_path: await realpath(process.execPath),
|
|
383
395
|
})
|
|
@@ -388,7 +400,7 @@ const PackageJsonSchema = z.looseObject({
|
|
|
388
400
|
})
|
|
389
401
|
|
|
390
402
|
/** The package a file belongs to: the nearest package.json with a name. */
|
|
391
|
-
const packageOf = async (
|
|
403
|
+
export const packageOf = async (
|
|
392
404
|
file: string
|
|
393
405
|
): Promise<{ name: string; version: string | null } | null> => {
|
|
394
406
|
for (let dir = dirname(file); ; dir = dirname(dir)) {
|
|
@@ -115,6 +115,8 @@ export type LucaInstall = {
|
|
|
115
115
|
luca_version: string
|
|
116
116
|
/** The board folder inside Luca's install folder. */
|
|
117
117
|
board_dir: string
|
|
118
|
+
/** The skills folder inside Luca's install folder (Luca's own skills). */
|
|
119
|
+
skills_dir: string
|
|
118
120
|
/** The real path of the installed `luca-run`. */
|
|
119
121
|
engine_path: string
|
|
120
122
|
/** Bun's own path. */
|
|
@@ -22,17 +22,24 @@ import {
|
|
|
22
22
|
type DoctorCheck,
|
|
23
23
|
type Found,
|
|
24
24
|
} from './doctor-checks'
|
|
25
|
+
import { claudeSkillsDir, skillDrift } from './luca-skills'
|
|
25
26
|
import { isRight, rightEntry, userMuninnEntry } from './muninn-entry'
|
|
26
27
|
|
|
27
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'
|
|
28
35
|
import { READY_LABEL } from '../tracker/tracker'
|
|
29
36
|
|
|
30
37
|
/**
|
|
31
38
|
* `luca doctor`'s checks of this computer, read-only: Bun, the `luca`
|
|
32
39
|
* copies on the PATH, Claude Code, the `gh` login, Paseo and its plugins,
|
|
33
|
-
* the board, MuninnDB and Claude Code's `muninn` entry,
|
|
34
|
-
* skills
|
|
35
|
-
*
|
|
40
|
+
* the board, MuninnDB and Claude Code's `muninn` entry, the planning
|
|
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.
|
|
36
43
|
*/
|
|
37
44
|
|
|
38
45
|
/** The oldest tool versions Luca works with. */
|
|
@@ -327,17 +334,77 @@ const checkPlanningSkills = async ({ home }: { home: string }) => {
|
|
|
327
334
|
return ok('The planning skills write what intake needs')
|
|
328
335
|
}
|
|
329
336
|
|
|
337
|
+
/**
|
|
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.
|
|
340
|
+
*/
|
|
341
|
+
const checkLucaSkills = async ({
|
|
342
|
+
home,
|
|
343
|
+
skills_dir,
|
|
344
|
+
luca_version,
|
|
345
|
+
}: {
|
|
346
|
+
home: string
|
|
347
|
+
skills_dir: string
|
|
348
|
+
luca_version: string
|
|
349
|
+
}) => {
|
|
350
|
+
const drift = await skillDrift({ home, skills_dir })
|
|
351
|
+
if (drift.length === 0) {
|
|
352
|
+
return problem({
|
|
353
|
+
detail: `Luca's folder ${skills_dir} has no skills.`,
|
|
354
|
+
fix: `Reinstall Luca: bun add -g ${LUCA_PACKAGE}@alpha`,
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
const off = drift.filter(
|
|
358
|
+
({ missing, changed }) => missing.length + changed.length > 0
|
|
359
|
+
)
|
|
360
|
+
if (off.length > 0) {
|
|
361
|
+
const described = off.map(({ skill, missing }) =>
|
|
362
|
+
missing.includes('SKILL.md')
|
|
363
|
+
? `/${skill} isn't installed`
|
|
364
|
+
: `/${skill} isn't the one Luca ${luca_version} ships`
|
|
365
|
+
)
|
|
366
|
+
return problem({
|
|
367
|
+
detail: `In ${claudeSkillsDir({ home })}, ${described.join('; ')}.`,
|
|
368
|
+
fix: 'Run luca doctor --fix (or luca init) to install it.',
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
return ok(
|
|
372
|
+
`Luca's skills (${drift.map(({ skill }) => `/${skill}`).join(', ')}) match Luca ${luca_version}`
|
|
373
|
+
)
|
|
374
|
+
}
|
|
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
|
+
|
|
330
396
|
/**
|
|
331
397
|
* Runs the computer checks, in order, read-only. Never throws: a check that
|
|
332
398
|
* fails becomes a problem.
|
|
333
399
|
*
|
|
334
400
|
* @example
|
|
335
|
-
* const checks = await computerChecks({ home, luca_version, board_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 })
|
|
336
402
|
*/
|
|
337
403
|
export const computerChecks = async ({
|
|
338
404
|
home,
|
|
339
405
|
luca_version,
|
|
340
406
|
board_dir,
|
|
407
|
+
skills_dir,
|
|
341
408
|
engine_path,
|
|
342
409
|
bun_path,
|
|
343
410
|
computer,
|
|
@@ -345,11 +412,14 @@ export const computerChecks = async ({
|
|
|
345
412
|
muninn_health,
|
|
346
413
|
claude,
|
|
347
414
|
paseo,
|
|
415
|
+
env,
|
|
348
416
|
}: {
|
|
349
417
|
home: string
|
|
350
418
|
/** The installed Luca's version. */
|
|
351
419
|
luca_version: string
|
|
352
420
|
board_dir: string
|
|
421
|
+
/** The skills folder inside Luca's install folder. */
|
|
422
|
+
skills_dir: string
|
|
353
423
|
engine_path: string
|
|
354
424
|
bun_path: string
|
|
355
425
|
computer: Computer
|
|
@@ -357,6 +427,11 @@ export const computerChecks = async ({
|
|
|
357
427
|
muninn_health: MuninnHealth
|
|
358
428
|
claude: ClaudeMcp
|
|
359
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
|
|
360
435
|
}): Promise<DoctorCheck[]> => {
|
|
361
436
|
// With Paseo down, its plugins and the board aren't asked about.
|
|
362
437
|
let paseo_up = true
|
|
@@ -399,6 +474,14 @@ export const computerChecks = async ({
|
|
|
399
474
|
['muninndb', () => checkMuninnDb({ muninn, muninn_health })],
|
|
400
475
|
['muninn_entry', () => checkMuninnEntry({ muninn, claude })],
|
|
401
476
|
['planning_skills', () => checkPlanningSkills({ home })],
|
|
477
|
+
[
|
|
478
|
+
'luca_skills',
|
|
479
|
+
() => checkLucaSkills({ home, skills_dir, luca_version }),
|
|
480
|
+
],
|
|
481
|
+
[
|
|
482
|
+
'decision_model',
|
|
483
|
+
() => checkDecisionModel({ env: env ?? {}, home }),
|
|
484
|
+
],
|
|
402
485
|
],
|
|
403
486
|
})
|
|
404
487
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The log line for a resume's config reload (#516): what a `config_reloaded`
|
|
3
|
+
* changed, or why a `config_reload_refused` kept the run's own config.
|
|
4
|
+
*/
|
|
5
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
6
|
+
|
|
7
|
+
/** A value of a reloaded config field in a log line; `null` is "none". */
|
|
8
|
+
const fieldValue = (value: string | number | string[] | null): string => {
|
|
9
|
+
if (value === null) return 'none'
|
|
10
|
+
if (Array.isArray(value)) return `[${value.join(', ')}]`
|
|
11
|
+
return String(value)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The log line for a resume's `config_reloaded` or `config_reload_refused`
|
|
16
|
+
* (#516).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* configReloadText({ reload }) // 'config reloaded: prepare changed (make rom -> make rom-cached)'
|
|
20
|
+
*/
|
|
21
|
+
export const configReloadText = ({
|
|
22
|
+
reload,
|
|
23
|
+
}: {
|
|
24
|
+
reload: JournalRecord
|
|
25
|
+
}): string => {
|
|
26
|
+
switch (reload.kind) {
|
|
27
|
+
case 'config_reloaded':
|
|
28
|
+
return `config reloaded: ${reload.content.changes
|
|
29
|
+
.map(
|
|
30
|
+
({ field, from, to }) =>
|
|
31
|
+
`${field} changed (${fieldValue(from)} -> ${fieldValue(to)})`
|
|
32
|
+
)
|
|
33
|
+
.join(', ')}`
|
|
34
|
+
case 'config_reload_refused':
|
|
35
|
+
return `kept the run's config, as the repo's can't be used: ${reload.content.reason}`
|
|
36
|
+
default:
|
|
37
|
+
return reload.kind
|
|
38
|
+
}
|
|
39
|
+
}
|