@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
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.
|
|
@@ -26,6 +30,8 @@
|
|
|
26
30
|
*/
|
|
27
31
|
import { homedir } from 'node:os'
|
|
28
32
|
|
|
33
|
+
import { defaultRegistryPath, goingRuns } from './going-runs'
|
|
34
|
+
import { listProcesses } from './live-runs'
|
|
29
35
|
import { parseRunArgs, type RunArgs } from './run-args'
|
|
30
36
|
import {
|
|
31
37
|
DEMO_TURN_DELAY_MS,
|
|
@@ -39,7 +45,7 @@ import {
|
|
|
39
45
|
import { createClaudeLauncher } from '../agents/claude-launcher'
|
|
40
46
|
import { createBoardSync, type BoardSync } from '../board/board-sync'
|
|
41
47
|
import { createPaseoBoardLink } from '../board/paseo-board-link'
|
|
42
|
-
import {
|
|
48
|
+
import { loadDecisionModelCredentials } from '../jev/decision-model-credentials'
|
|
43
49
|
import { defaultRunsDir } from '../journal/journal'
|
|
44
50
|
import {
|
|
45
51
|
defaultSharedReadingsPath,
|
|
@@ -76,6 +82,28 @@ const memoryOf = async (): Promise<MemoryDeps | undefined> => {
|
|
|
76
82
|
return { client: createMuninnMcpClient({ settings: found.settings }) }
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
/** The decision model's setup: its credentials from Luca's own env file. */
|
|
86
|
+
const decisionModelSetup = async () => ({
|
|
87
|
+
credentials: await loadDecisionModelCredentials({
|
|
88
|
+
env: process.env,
|
|
89
|
+
home: homedir(),
|
|
90
|
+
}),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The unfinished runs on this computer, for the repo wait (#615): every
|
|
95
|
+
* journal in the runs folder and the board's registry, checked with `ps`.
|
|
96
|
+
*/
|
|
97
|
+
const goingRunsNow = () =>
|
|
98
|
+
goingRuns({
|
|
99
|
+
runs_dir: defaultRunsDir(),
|
|
100
|
+
registry_path: defaultRegistryPath({
|
|
101
|
+
env: process.env,
|
|
102
|
+
home_dir: homedir(),
|
|
103
|
+
}),
|
|
104
|
+
list_processes: listProcesses,
|
|
105
|
+
})
|
|
106
|
+
|
|
79
107
|
/**
|
|
80
108
|
* The usage line (#434): the `luca-board` plugin's lines, and the readings
|
|
81
109
|
* every run shares, both in Luca's state folder.
|
|
@@ -117,17 +145,22 @@ const run = async ({
|
|
|
117
145
|
runs_dir: defaultRunsDir(),
|
|
118
146
|
repo: args.repo,
|
|
119
147
|
tracker: async ({ repo }) =>
|
|
120
|
-
createGitHubTracker({
|
|
148
|
+
createGitHubTracker({
|
|
149
|
+
repo: await githubRepoOf({ repo }),
|
|
150
|
+
log,
|
|
151
|
+
}),
|
|
121
152
|
launcher: createClaudeLauncher({}),
|
|
122
|
-
|
|
153
|
+
decision_model: await decisionModelSetup(),
|
|
123
154
|
memory: await memoryOf(),
|
|
124
155
|
usage: usageLine(),
|
|
156
|
+
going_runs: goingRunsNow,
|
|
125
157
|
board,
|
|
126
158
|
log,
|
|
127
159
|
})
|
|
128
160
|
}
|
|
129
161
|
const tracker = createGitHubTracker({
|
|
130
162
|
repo: await githubRepoOf({ repo: args.repo }),
|
|
163
|
+
log,
|
|
131
164
|
})
|
|
132
165
|
return runSpec({
|
|
133
166
|
spec_number: args.spec_number,
|
|
@@ -137,9 +170,11 @@ const run = async ({
|
|
|
137
170
|
runs_dir: defaultRunsDir(),
|
|
138
171
|
tracker,
|
|
139
172
|
launcher: createClaudeLauncher({}),
|
|
140
|
-
|
|
173
|
+
decision_model: await decisionModelSetup(),
|
|
141
174
|
memory: await memoryOf(),
|
|
142
175
|
usage: usageLine(),
|
|
176
|
+
going_runs: goingRunsNow,
|
|
177
|
+
alongside: args.alongside === true,
|
|
143
178
|
board,
|
|
144
179
|
log,
|
|
145
180
|
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { StepEnd } from './board-in-paseo'
|
|
5
|
+
import { reason } from './doctor-checks'
|
|
6
|
+
|
|
7
|
+
/**
|
|
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.
|
|
19
|
+
*
|
|
20
|
+
* A copy writes every file the skill ships (not its tests), over Luca's own
|
|
21
|
+
* earlier copy. It never deletes, and never touches other skills.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Test files stay in the repo: they aren't part of a skill. */
|
|
25
|
+
const isTestFile = (path: string) => /\.test\.[cm]?[jt]sx?$/.test(path)
|
|
26
|
+
|
|
27
|
+
/** Where Claude Code finds a user's own skills. */
|
|
28
|
+
export const claudeSkillsDir = ({ home }: { home: string }): string =>
|
|
29
|
+
join(home, '.claude', 'skills')
|
|
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
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The files one skill ships, relative to its folder in `skills_dir`,
|
|
58
|
+
* sorted: every file but tests. None when the folder is missing.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* await skillFiles({ skills_dir, skill: 'luca-unstick' }) // ['SKILL.md', 'scripts/stuck-summary.ts']
|
|
62
|
+
*/
|
|
63
|
+
export const skillFiles = async ({
|
|
64
|
+
skills_dir,
|
|
65
|
+
skill,
|
|
66
|
+
}: {
|
|
67
|
+
skills_dir: string
|
|
68
|
+
skill: string
|
|
69
|
+
}): Promise<string[]> => {
|
|
70
|
+
try {
|
|
71
|
+
const files = await Array.fromAsync(
|
|
72
|
+
new Bun.Glob('**/*').scan({
|
|
73
|
+
cwd: join(skills_dir, skill),
|
|
74
|
+
dot: true,
|
|
75
|
+
onlyFiles: true,
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
return files.filter((file) => !isTestFile(file)).toSorted()
|
|
79
|
+
} catch {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** How one installed skill differs from the one Luca ships. */
|
|
85
|
+
export type SkillDrift = {
|
|
86
|
+
skill: string
|
|
87
|
+
/** Shipped files the installed copy lacks. */
|
|
88
|
+
missing: string[]
|
|
89
|
+
/** Shipped files whose installed copy has other content. */
|
|
90
|
+
changed: string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* How each of Luca's skills in `~/.claude/skills` differs from the ones in
|
|
95
|
+
* `skills_dir`; a skill that matches has empty lists. None when
|
|
96
|
+
* `skills_dir` has no skills. Read-only.
|
|
97
|
+
*/
|
|
98
|
+
export const skillDrift = async ({
|
|
99
|
+
home,
|
|
100
|
+
skills_dir,
|
|
101
|
+
}: {
|
|
102
|
+
home: string
|
|
103
|
+
skills_dir: string
|
|
104
|
+
}): Promise<SkillDrift[]> => {
|
|
105
|
+
const drift: SkillDrift[] = []
|
|
106
|
+
for (const skill of await lucaSkills({ skills_dir })) {
|
|
107
|
+
const files = await skillFiles({ skills_dir, skill })
|
|
108
|
+
const missing: string[] = []
|
|
109
|
+
const changed: string[] = []
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
const installed = Bun.file(
|
|
112
|
+
join(claudeSkillsDir({ home }), skill, file)
|
|
113
|
+
)
|
|
114
|
+
if (!(await installed.exists())) {
|
|
115
|
+
missing.push(file)
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
const shipped = Bun.file(join(skills_dir, skill, file))
|
|
119
|
+
if ((await installed.text()) !== (await shipped.text())) {
|
|
120
|
+
changed.push(file)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
drift.push({ skill, missing, changed })
|
|
124
|
+
}
|
|
125
|
+
return drift
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Copies Luca's skills from `skills_dir` into `~/.claude/skills`, over
|
|
130
|
+
* Luca's own earlier copy: each shipped file whose installed copy is
|
|
131
|
+
* missing or differs is written. Logs one line after `prefix`. Never
|
|
132
|
+
* throws.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* await installLucaSkills({ home: homedir(), skills_dir, prefix: '[luca init]', log: console.log })
|
|
136
|
+
* // [luca init] Luca's skills: installed /luca-retro, /luca-unstick in ~/.claude/skills
|
|
137
|
+
*/
|
|
138
|
+
export const installLucaSkills = async ({
|
|
139
|
+
home,
|
|
140
|
+
skills_dir,
|
|
141
|
+
prefix,
|
|
142
|
+
log,
|
|
143
|
+
}: {
|
|
144
|
+
home: string
|
|
145
|
+
skills_dir: string
|
|
146
|
+
prefix: string
|
|
147
|
+
log: (line: string) => void
|
|
148
|
+
}): Promise<StepEnd> => {
|
|
149
|
+
const say = ({ message, ok }: StepEnd): StepEnd => {
|
|
150
|
+
log(message)
|
|
151
|
+
return { ok, message }
|
|
152
|
+
}
|
|
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
|
+
}
|
|
161
|
+
const written: string[] = []
|
|
162
|
+
for (const { skill, missing, changed } of drift) {
|
|
163
|
+
for (const file of [...missing, ...changed]) {
|
|
164
|
+
const target = join(claudeSkillsDir({ home }), skill, file)
|
|
165
|
+
await mkdir(dirname(target), { recursive: true })
|
|
166
|
+
await Bun.write(target, Bun.file(join(skills_dir, skill, file)))
|
|
167
|
+
}
|
|
168
|
+
if (missing.length + changed.length > 0) written.push(skill)
|
|
169
|
+
}
|
|
170
|
+
return say({
|
|
171
|
+
message:
|
|
172
|
+
written.length === 0
|
|
173
|
+
? `${prefix} Luca's skills: up to date`
|
|
174
|
+
: `${prefix} Luca's skills: installed ${written.map((skill) => `/${skill}`).join(', ')} in ${claudeSkillsDir({ home })}`,
|
|
175
|
+
ok: true,
|
|
176
|
+
})
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return say({
|
|
179
|
+
message: `${prefix} Luca's skills: ${reason(error)}`,
|
|
180
|
+
ok: false,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
package/engine/cli/luca.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* luca upgrade [--to <version>] moves to another version of Luca
|
|
10
10
|
* luca doctor [--fix] checks this computer and repo; --fix
|
|
11
11
|
* fixes what's safe
|
|
12
|
+
* luca models scores each routed role's model from
|
|
13
|
+
* this repo's runs
|
|
12
14
|
* luca hook <anything> does nothing, quietly
|
|
13
15
|
*
|
|
14
16
|
* `luca hook` is for old Luca (v13), whose global Claude Code hook runs
|
|
@@ -26,7 +28,8 @@ Commands:
|
|
|
26
28
|
Set up this computer for Luca (once), then the repo you're in
|
|
27
29
|
setup [--base <branch>] Get the repo in this folder ready for Luca
|
|
28
30
|
upgrade [--to <version>] Move to the newest Luca on your channel, or to <version>
|
|
29
|
-
doctor [--fix] Check this computer and repo; --fix fixes what's safe
|
|
31
|
+
doctor [--fix] Check this computer and repo; --fix fixes what's safe
|
|
32
|
+
models Score each routed role's model from this repo's runs`
|
|
30
33
|
|
|
31
34
|
const main = async (): Promise<number> => {
|
|
32
35
|
const [command, ...rest] = Bun.argv.slice(2)
|
|
@@ -49,6 +52,10 @@ const main = async (): Promise<number> => {
|
|
|
49
52
|
const { doctorCommand } = await import('./doctor-command')
|
|
50
53
|
return doctorCommand({ argv: rest })
|
|
51
54
|
}
|
|
55
|
+
case 'models': {
|
|
56
|
+
const { modelsCommand } = await import('./models-command')
|
|
57
|
+
return modelsCommand({ argv: rest })
|
|
58
|
+
}
|
|
52
59
|
case '--help':
|
|
53
60
|
case '-h':
|
|
54
61
|
case 'help':
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `luca models`: scores each routed role's model from the ticket review's
|
|
3
|
+
* findings over this repo's runs, and prints the report (#605):
|
|
4
|
+
*
|
|
5
|
+
* luca models
|
|
6
|
+
*
|
|
7
|
+
* It only reads: it never writes config or journals. A run is this repo's
|
|
8
|
+
* when its `run_started` repo is the main checkout or git top folder of the
|
|
9
|
+
* current folder. Exits 0, 2 on bad flags.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs'
|
|
12
|
+
import { basename, dirname, resolve } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { defaultRunsDir, readRecords, runJournalPath } from '../journal/journal'
|
|
15
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
16
|
+
import { formatModelReport, scoreModels } from '../models/model-score'
|
|
17
|
+
|
|
18
|
+
/** `luca models`'s usage line. */
|
|
19
|
+
export const MODELS_USAGE = 'Usage: luca models'
|
|
20
|
+
|
|
21
|
+
/** A path with its symlinks followed, or made absolute when it's gone. */
|
|
22
|
+
const realPath = (path: string): string => {
|
|
23
|
+
try {
|
|
24
|
+
return realpathSync(path)
|
|
25
|
+
} catch {
|
|
26
|
+
return resolve(path)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The repos a run started from `cwd` may name, real paths: the main
|
|
32
|
+
* checkout (a worktree's too), then the git top folder. Outside git, `cwd`.
|
|
33
|
+
*/
|
|
34
|
+
const currentRepos = ({ cwd }: { cwd: string }): string[] => {
|
|
35
|
+
const git = (...args: string[]) => {
|
|
36
|
+
const out = Bun.spawnSync(['git', '-C', cwd, ...args])
|
|
37
|
+
const said = out.stdout.toString().trim()
|
|
38
|
+
return out.exitCode === 0 && said !== '' ? said : null
|
|
39
|
+
}
|
|
40
|
+
const top = git('rev-parse', '--show-toplevel')
|
|
41
|
+
const common = git(
|
|
42
|
+
'rev-parse',
|
|
43
|
+
'--path-format=absolute',
|
|
44
|
+
'--git-common-dir'
|
|
45
|
+
)
|
|
46
|
+
const main =
|
|
47
|
+
common !== null && basename(common) === '.git' ? dirname(common) : null
|
|
48
|
+
return [
|
|
49
|
+
...new Set(
|
|
50
|
+
[main, top ?? cwd]
|
|
51
|
+
.filter((repo) => repo !== null)
|
|
52
|
+
.map((repo) => realPath(repo))
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The records of every run in `runs_dir` that started in one of `repos`,
|
|
59
|
+
* oldest run id first. A journal that can't be read is left out, with a
|
|
60
|
+
* line on `warn`.
|
|
61
|
+
*/
|
|
62
|
+
const repoRuns = ({
|
|
63
|
+
runs_dir,
|
|
64
|
+
repos,
|
|
65
|
+
warn,
|
|
66
|
+
}: {
|
|
67
|
+
runs_dir: string
|
|
68
|
+
repos: string[]
|
|
69
|
+
warn: (line: string) => void
|
|
70
|
+
}): JournalRecord[][] => {
|
|
71
|
+
if (!existsSync(runs_dir)) return []
|
|
72
|
+
const want = new Set(repos)
|
|
73
|
+
return readdirSync(runs_dir, { withFileTypes: true })
|
|
74
|
+
.filter((entry) => entry.isDirectory())
|
|
75
|
+
.map(({ name }) => name)
|
|
76
|
+
.toSorted()
|
|
77
|
+
.flatMap((run_id) => {
|
|
78
|
+
let records: JournalRecord[]
|
|
79
|
+
try {
|
|
80
|
+
records = readRecords({
|
|
81
|
+
file: runJournalPath({ runs_dir, run_id }),
|
|
82
|
+
})
|
|
83
|
+
} catch (error) {
|
|
84
|
+
warn(
|
|
85
|
+
`Left out run ${run_id}: ${error instanceof Error ? error.message : String(error)}`
|
|
86
|
+
)
|
|
87
|
+
return []
|
|
88
|
+
}
|
|
89
|
+
const [first] = records
|
|
90
|
+
return first?.kind === 'run_started' &&
|
|
91
|
+
first.content.repo !== null &&
|
|
92
|
+
want.has(realPath(first.content.repo))
|
|
93
|
+
? [records]
|
|
94
|
+
: []
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Runs `luca models` with the flags after `models`; returns the exit code. */
|
|
99
|
+
export const modelsCommand = ({ argv }: { argv: string[] }): number => {
|
|
100
|
+
if (argv.length > 0) {
|
|
101
|
+
console.error(MODELS_USAGE)
|
|
102
|
+
return 2
|
|
103
|
+
}
|
|
104
|
+
const runs_dir = defaultRunsDir()
|
|
105
|
+
const repos = currentRepos({ cwd: process.cwd() })
|
|
106
|
+
const runs = repoRuns({
|
|
107
|
+
runs_dir,
|
|
108
|
+
repos,
|
|
109
|
+
warn: (line) => {
|
|
110
|
+
console.error(`[luca models] ${line}`)
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
if (runs.length === 0) {
|
|
114
|
+
console.log(`No Luca runs of ${repos.join(' or ')} in ${runs_dir}.`)
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
console.log(formatModelReport({ report: scoreModels({ runs }) }))
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a run's journal to go on from it: the records of a run in the runs
|
|
3
|
+
* folder (`readRun`), and its `run_started` (`readRunStart`, `RunStart`).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
|
|
7
|
+
import { errorText } from './run-end'
|
|
8
|
+
|
|
9
|
+
import { createJournal, runJournalPath } from '../journal/journal'
|
|
10
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
11
|
+
|
|
12
|
+
/** What a run's `run_started` says: its spec, base branch, repo, and time. */
|
|
13
|
+
export type RunStart = {
|
|
14
|
+
spec_number: number
|
|
15
|
+
base_branch: string
|
|
16
|
+
/** `null` in journals from before the repo was kept. */
|
|
17
|
+
repo: string | null
|
|
18
|
+
/** When `run_started` was journaled. */
|
|
19
|
+
started_at: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The records of run `run_id`, or why there are none to go on from: no
|
|
24
|
+
* journal, an empty one, or one that cannot be read. Never makes the run's
|
|
25
|
+
* folder.
|
|
26
|
+
*/
|
|
27
|
+
export const readRun = ({
|
|
28
|
+
runs_dir,
|
|
29
|
+
run_id,
|
|
30
|
+
}: {
|
|
31
|
+
runs_dir: string
|
|
32
|
+
run_id: string
|
|
33
|
+
}): { ok: true; records: JournalRecord[] } | { ok: false; error: string } => {
|
|
34
|
+
const file = runJournalPath({ runs_dir, run_id })
|
|
35
|
+
if (!existsSync(file)) {
|
|
36
|
+
return { ok: false, error: `No run ${run_id}: there is no ${file}.` }
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const records = createJournal({ file }).read()
|
|
40
|
+
return records.length === 0
|
|
41
|
+
? {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: `Run ${run_id} has an empty journal (${file}), so there is nothing to go on from; start a new run with --spec.`,
|
|
44
|
+
}
|
|
45
|
+
: { ok: true, records }
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return { ok: false, error: errorText(error) }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reads run `run_id`'s `run_started`: the spec, base branch, and repo a
|
|
53
|
+
* resume goes on with. An error when the run has no journal, an empty one,
|
|
54
|
+
* or one that does not start with `run_started`.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const start = readRunStart({ runs_dir: defaultRunsDir(), run_id })
|
|
58
|
+
* if (start.ok) console.log(start.start.spec_number)
|
|
59
|
+
*/
|
|
60
|
+
export const readRunStart = ({
|
|
61
|
+
runs_dir,
|
|
62
|
+
run_id,
|
|
63
|
+
}: {
|
|
64
|
+
runs_dir: string
|
|
65
|
+
run_id: string
|
|
66
|
+
}): { ok: true; start: RunStart } | { ok: false; error: string } => {
|
|
67
|
+
const read = readRun({ runs_dir, run_id })
|
|
68
|
+
if (!read.ok) return read
|
|
69
|
+
const [first] = read.records
|
|
70
|
+
if (first?.kind !== 'run_started') {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: `Run ${run_id}'s journal does not start with run_started, so it is not a run.`,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const { spec_number, base_branch, repo } = first.content
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
start: { spec_number, base_branch, repo, started_at: first.time },
|
|
80
|
+
}
|
|
81
|
+
}
|
package/engine/cli/run-args.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const BOARD_TOKEN_ENV = 'LUCA_BOARD_TOKEN'
|
|
|
10
10
|
|
|
11
11
|
/** How to call `luca-run`, shown with every argument error. */
|
|
12
12
|
export const RUN_USAGE = `Usage:
|
|
13
|
-
luca-run --spec <n> [--repo <path>] [--run-id <id>] [--base <branch>] [--board-plugin <id>]
|
|
13
|
+
luca-run --spec <n> [--repo <path>] [--run-id <id>] [--base <branch>] [--alongside] [--board-plugin <id>]
|
|
14
14
|
luca-run --demo [--run-id <id>] [--board-plugin <id>]
|
|
15
15
|
luca-run --resume <run-id> [--repo <path>] [--board-plugin <id>]
|
|
16
16
|
luca-run --unfinished
|
|
@@ -26,6 +26,8 @@ export const RUN_USAGE = `Usage:
|
|
|
26
26
|
(for --resume, to the repo the run started in).
|
|
27
27
|
--run-id <id> The run's id and folder name. Defaults to a new one.
|
|
28
28
|
--base <branch> The branch the run starts from. Defaults to main.
|
|
29
|
+
--alongside Start now, even while an earlier run on the repo is
|
|
30
|
+
going. Without it, the run waits for that run first.
|
|
29
31
|
--board-plugin <id> Send the journal to this Paseo board plugin; the
|
|
30
32
|
per-run token comes from $${BOARD_TOKEN_ENV}.`
|
|
31
33
|
|
|
@@ -67,6 +69,7 @@ const resumeArgsSchema = ({ cwd }: { cwd: string }) =>
|
|
|
67
69
|
demo: z.undefined('Give only one of --resume or --demo.'),
|
|
68
70
|
run_id: z.undefined('--resume <run-id> is the run id.'),
|
|
69
71
|
base: z.undefined('--resume takes the base from the journal.'),
|
|
72
|
+
alongside: z.undefined('--alongside is for a new run (--spec).'),
|
|
70
73
|
})
|
|
71
74
|
.refine(({ board_plugin, token }) => board_plugin === null || token, {
|
|
72
75
|
message: BOARD_TOKEN_MESSAGE,
|
|
@@ -91,6 +94,7 @@ const unfinishedArgsSchema = z
|
|
|
91
94
|
repo: z.undefined(UNFINISHED_ALONE),
|
|
92
95
|
run_id: z.undefined(UNFINISHED_ALONE),
|
|
93
96
|
base: z.undefined(UNFINISHED_ALONE),
|
|
97
|
+
alongside: z.undefined(UNFINISHED_ALONE),
|
|
94
98
|
board_plugin: z.undefined(UNFINISHED_ALONE),
|
|
95
99
|
})
|
|
96
100
|
.transform((): RunArgs => ({ mode: 'unfinished' }))
|
|
@@ -107,26 +111,42 @@ const runArgsSchema = ({ cwd }: { cwd: string }) =>
|
|
|
107
111
|
.transform((path) => resolve(cwd, path)),
|
|
108
112
|
run_id: RunIdSchema.default(() => makeRunId()),
|
|
109
113
|
base: z.string().min(1).nullable().default(null),
|
|
114
|
+
alongside: z.boolean().default(false),
|
|
110
115
|
board_plugin: z.string().min(1).nullable().default(null),
|
|
111
116
|
token: z.string().nullable().default(null),
|
|
112
117
|
})
|
|
113
118
|
.refine(({ spec, demo }) => (spec === undefined) === demo, {
|
|
114
119
|
message: 'Give exactly one of --spec <n> or --demo.',
|
|
115
120
|
})
|
|
121
|
+
.refine(({ demo, alongside }) => !(demo && alongside), {
|
|
122
|
+
message: '--alongside is for a run of a spec (--spec).',
|
|
123
|
+
})
|
|
116
124
|
.refine(({ board_plugin, token }) => board_plugin === null || token, {
|
|
117
125
|
message: BOARD_TOKEN_MESSAGE,
|
|
118
126
|
})
|
|
119
127
|
.transform(
|
|
120
|
-
({
|
|
128
|
+
({
|
|
129
|
+
spec,
|
|
130
|
+
repo,
|
|
131
|
+
run_id,
|
|
132
|
+
base,
|
|
133
|
+
alongside,
|
|
134
|
+
board_plugin,
|
|
135
|
+
token,
|
|
136
|
+
}): RunArgs => {
|
|
121
137
|
const common = {
|
|
122
138
|
repo,
|
|
123
139
|
run_id,
|
|
124
140
|
base_branch: base,
|
|
125
141
|
board: boardOf({ board_plugin, token }),
|
|
126
142
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
143
|
+
if (spec === undefined) return { mode: 'demo', ...common }
|
|
144
|
+
return {
|
|
145
|
+
mode: 'spec',
|
|
146
|
+
spec_number: spec,
|
|
147
|
+
...common,
|
|
148
|
+
...(alongside ? { alongside: true } : {}),
|
|
149
|
+
}
|
|
130
150
|
}
|
|
131
151
|
)
|
|
132
152
|
|
|
@@ -142,7 +162,12 @@ type RunArgsCommon = {
|
|
|
142
162
|
|
|
143
163
|
/** What `luca-run` was asked to do. */
|
|
144
164
|
export type RunArgs =
|
|
145
|
-
| (RunArgsCommon & {
|
|
165
|
+
| (RunArgsCommon & {
|
|
166
|
+
mode: 'spec'
|
|
167
|
+
spec_number: number
|
|
168
|
+
/** Skip the repo wait (`--alongside`); left out, the run waits. */
|
|
169
|
+
alongside?: true
|
|
170
|
+
})
|
|
146
171
|
| (RunArgsCommon & { mode: 'demo' })
|
|
147
172
|
| {
|
|
148
173
|
mode: 'resume'
|
|
@@ -189,6 +214,7 @@ export const parseRunArgs = ({
|
|
|
189
214
|
repo: { type: 'string' },
|
|
190
215
|
'run-id': { type: 'string' },
|
|
191
216
|
base: { type: 'string' },
|
|
217
|
+
alongside: { type: 'boolean' },
|
|
192
218
|
'board-plugin': { type: 'string' },
|
|
193
219
|
},
|
|
194
220
|
}).values
|
|
@@ -210,6 +236,7 @@ export const parseRunArgs = ({
|
|
|
210
236
|
repo: values.repo,
|
|
211
237
|
run_id: values['run-id'],
|
|
212
238
|
base: values.base,
|
|
239
|
+
alongside: values.alongside,
|
|
213
240
|
board_plugin: values['board-plugin'],
|
|
214
241
|
token: env[BOARD_TOKEN_ENV],
|
|
215
242
|
})
|