@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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runs in the runs folder that are not over, and whether Paseo may
|
|
3
|
+
* restart each one (#375): `unfinishedRuns`, `restartableRuns`, and the shape
|
|
4
|
+
* `luca-run --unfinished` prints.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
|
|
10
|
+
import { readRun } from './read-run'
|
|
11
|
+
|
|
12
|
+
import { decide } from '../core/decide'
|
|
13
|
+
import { STOP_ACTIONS } from '../core/execute'
|
|
14
|
+
import type { JournalRecord } from '../journal/journal-record'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The ids of the runs in `runs_dir` that are not over: runs whose journal's
|
|
18
|
+
* next action (by the decision step) is not a stop (`done` or
|
|
19
|
+
* `invalid_journal`), such as a run that crashed, was killed, or waits for a
|
|
20
|
+
* reply. Folders with no journal, an empty one, or one that cannot be read
|
|
21
|
+
* are left out. Sorted, so oldest first for made-up run ids. Paseo can
|
|
22
|
+
* `resumeRun` each one when it starts (#375).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* for (const run_id of unfinishedRuns({ runs_dir: defaultRunsDir() })) console.log(run_id)
|
|
26
|
+
*/
|
|
27
|
+
export const unfinishedRuns = ({
|
|
28
|
+
runs_dir,
|
|
29
|
+
}: {
|
|
30
|
+
runs_dir: string
|
|
31
|
+
}): string[] => {
|
|
32
|
+
if (!existsSync(runs_dir)) return []
|
|
33
|
+
return readdirSync(runs_dir, { withFileTypes: true })
|
|
34
|
+
.filter((entry) => entry.isDirectory())
|
|
35
|
+
.map(({ name }) => name)
|
|
36
|
+
.filter((run_id) => {
|
|
37
|
+
const read = readRun({ runs_dir, run_id })
|
|
38
|
+
return (
|
|
39
|
+
read.ok &&
|
|
40
|
+
!STOP_ACTIONS.has(decide({ records: read.records }).type)
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
.toSorted()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One unfinished run, as `luca-run --unfinished` prints it for the board
|
|
48
|
+
* plugin (#375): whether Paseo may restart it with `--resume`, and why.
|
|
49
|
+
*
|
|
50
|
+
* - `resumable`: the engine was cut off (a crash, a kill); `restart` is true.
|
|
51
|
+
* - `launcher_stopped`: its last record is a launcher stop (the wrong login,
|
|
52
|
+
* plan, or model). A restart would stop the same way, so `restart` is
|
|
53
|
+
* false; `message` is the stop's reason.
|
|
54
|
+
* - `billing_stopped`: it stopped for billing, which sticks; `restart` is
|
|
55
|
+
* false and `message` is the stop's reason.
|
|
56
|
+
*/
|
|
57
|
+
export const RestartableRunSchema = z.object({
|
|
58
|
+
run_id: z.string(),
|
|
59
|
+
restart: z.boolean(),
|
|
60
|
+
reason: z.enum(['resumable', 'launcher_stopped', 'billing_stopped']),
|
|
61
|
+
message: z.string().nullable(),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export type RestartableRun = z.infer<typeof RestartableRunSchema>
|
|
65
|
+
|
|
66
|
+
/** What `luca-run --unfinished` prints: one JSON object, snake_case keys. */
|
|
67
|
+
export const UnfinishedOutputSchema = z.object({
|
|
68
|
+
runs: z.array(RestartableRunSchema),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
/** Whether Paseo may restart one unfinished run, from its records. */
|
|
72
|
+
const restartOf = ({
|
|
73
|
+
run_id,
|
|
74
|
+
records,
|
|
75
|
+
}: {
|
|
76
|
+
run_id: string
|
|
77
|
+
records: JournalRecord[]
|
|
78
|
+
}): RestartableRun => {
|
|
79
|
+
// A billing stop sticks, wherever it is in the journal.
|
|
80
|
+
const billing = records.find(
|
|
81
|
+
(record) => record.kind === 'run_stopped' && record.content.billing
|
|
82
|
+
)
|
|
83
|
+
if (billing?.kind === 'run_stopped') {
|
|
84
|
+
return {
|
|
85
|
+
run_id,
|
|
86
|
+
restart: false,
|
|
87
|
+
reason: 'billing_stopped',
|
|
88
|
+
message: billing.content.reason,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const last = records.at(-1)
|
|
92
|
+
if (last?.kind === 'run_stopped' && !last.content.crashed) {
|
|
93
|
+
return {
|
|
94
|
+
run_id,
|
|
95
|
+
restart: false,
|
|
96
|
+
reason: 'launcher_stopped',
|
|
97
|
+
message: last.content.reason,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { run_id, restart: true, reason: 'resumable', message: null }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The unfinished runs in `runs_dir` (`unfinishedRuns`), each with whether
|
|
105
|
+
* Paseo may restart it with `--resume` (#375). A run whose last record is a
|
|
106
|
+
* launcher stop is not restarted, since it would stop the same way; a run
|
|
107
|
+
* with a billing stop anywhere in its journal never is. Everything else was
|
|
108
|
+
* cut off and can go on from its journal.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* const runs = restartableRuns({ runs_dir: defaultRunsDir() })
|
|
112
|
+
* // [{ run_id: 'luca-20260923-141500-ab12', restart: true, reason: 'resumable', message: null }]
|
|
113
|
+
*/
|
|
114
|
+
export const restartableRuns = ({
|
|
115
|
+
runs_dir,
|
|
116
|
+
}: {
|
|
117
|
+
runs_dir: string
|
|
118
|
+
}): RestartableRun[] =>
|
|
119
|
+
unfinishedRuns({ runs_dir }).flatMap((run_id) => {
|
|
120
|
+
const read = readRun({ runs_dir, run_id })
|
|
121
|
+
return read.ok ? [restartOf({ run_id, records: read.records })] : []
|
|
122
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `luca upgrade`: moves this computer to another published version of
|
|
3
|
-
* Luca, never while a run is going
|
|
3
|
+
* Luca, never while a run is going (its engine is running, or the board
|
|
4
|
+
* will restart it):
|
|
4
5
|
*
|
|
5
6
|
* luca upgrade [--to <version>]
|
|
6
7
|
*
|
|
@@ -10,24 +11,45 @@
|
|
|
10
11
|
* board in Paseo, keeping its settings, and ends with `luca doctor`'s
|
|
11
12
|
* computer checks. See `runUpgrade`.
|
|
12
13
|
*
|
|
14
|
+
* Those last steps run in the version just installed (#529): upgrade
|
|
15
|
+
* hands off to it, as
|
|
16
|
+
*
|
|
17
|
+
* luca upgrade --finish [--board-before <fingerprint>]
|
|
18
|
+
*
|
|
19
|
+
* which only Luca runs. It doesn't check for going runs or install
|
|
20
|
+
* anything: it reloads the board, copies Luca's skills, and runs the
|
|
21
|
+
* checks, with its own code. See `finishUpgrade`.
|
|
22
|
+
*
|
|
13
23
|
* Exits 0 when done, 1 when it refused or a step failed, 2 on bad flags.
|
|
14
24
|
*/
|
|
25
|
+
import { realpath } from 'node:fs/promises'
|
|
15
26
|
import { homedir } from 'node:os'
|
|
27
|
+
import { dirname, join } from 'node:path'
|
|
16
28
|
|
|
17
29
|
import { z } from 'zod'
|
|
18
30
|
|
|
31
|
+
import type { Paseo } from './computer-adapters'
|
|
19
32
|
import {
|
|
20
33
|
claudeOf,
|
|
21
34
|
computerOf,
|
|
22
35
|
lucaInstall,
|
|
23
36
|
muninnHealth,
|
|
24
37
|
muninnOf,
|
|
38
|
+
packageOf,
|
|
25
39
|
paseoOf,
|
|
26
40
|
} from './computer-adapters-real'
|
|
27
41
|
import { computerChecks } from './computer-checks'
|
|
28
42
|
import { reason } from './doctor-checks'
|
|
29
43
|
import { defaultRegistryPath } from './going-runs'
|
|
30
|
-
import {
|
|
44
|
+
import { listProcesses } from './live-runs'
|
|
45
|
+
import {
|
|
46
|
+
runUpgrade,
|
|
47
|
+
type BunGlobal,
|
|
48
|
+
type HandOff,
|
|
49
|
+
type NewInstall,
|
|
50
|
+
type NpmRegistry,
|
|
51
|
+
} from './upgrade'
|
|
52
|
+
import { finishUpgrade } from './upgrade-finish'
|
|
31
53
|
|
|
32
54
|
import { LUCA_PACKAGE } from '../config/luca-version'
|
|
33
55
|
import { defaultRunsDir } from '../journal/journal'
|
|
@@ -75,60 +97,220 @@ const bunOf = ({ home }: { home: string }): BunGlobal => ({
|
|
|
75
97
|
},
|
|
76
98
|
})
|
|
77
99
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
/**
|
|
101
|
+
* The new install's file older Luca looks for (see `upgrade-finish.ts`):
|
|
102
|
+
* next to its `luca.ts` when it can run `luca upgrade --finish`.
|
|
103
|
+
*/
|
|
104
|
+
const FINISH_MODULE = 'upgrade-finish.ts'
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The new install's `luca` entry, when it is `version` of Luca and can
|
|
108
|
+
* finish an upgrade; otherwise why not, in one sentence without a period.
|
|
109
|
+
* Finds it the way the PATH does: the `luca` in Bun's global commands
|
|
110
|
+
* folder (`bun pm bin -g`), which `bun add -g` points at the new install.
|
|
111
|
+
* Read-only.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* await findNewInstall({ bin_dir: '/Users/me/.bun/bin', version: '14.0.0-alpha.11' })
|
|
115
|
+
* // { ok: true, entry: '/Users/me/.bun/install/global/node_modules/@alecsibilia/luca/engine/cli/luca.ts' }
|
|
116
|
+
*/
|
|
117
|
+
export const findNewInstall = async ({
|
|
118
|
+
bin_dir,
|
|
119
|
+
version,
|
|
120
|
+
}: {
|
|
121
|
+
bin_dir: string
|
|
122
|
+
version: string
|
|
123
|
+
}): Promise<{ ok: true; entry: string } | { ok: false; why: string }> => {
|
|
124
|
+
const entry = await realpath(join(bin_dir, 'luca')).catch(() => null)
|
|
125
|
+
if (entry === null) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
why: `Bun's global commands folder ${bin_dir} has no luca`,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const found = await packageOf(entry)
|
|
132
|
+
if (found?.name !== LUCA_PACKAGE || found.version !== version) {
|
|
133
|
+
const what =
|
|
134
|
+
found === null ? entry : `${found.name}@${found.version ?? '?'}`
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
why: `The luca in ${bin_dir} is ${what}, not ${LUCA_PACKAGE}@${version}`,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!(await Bun.file(join(dirname(entry), FINISH_MODULE)).exists())) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
why: `Luca ${version} can't finish an upgrade itself`,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { ok: true, entry }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Runs `luca upgrade --finish` from `entry` with `bun_path`, its output
|
|
151
|
+
* going straight to this terminal; resolves to its exit code. Throws when
|
|
152
|
+
* it can't be started.
|
|
153
|
+
*/
|
|
154
|
+
export const handOff = async ({
|
|
155
|
+
bun_path,
|
|
156
|
+
entry,
|
|
157
|
+
board_before,
|
|
158
|
+
cwd,
|
|
159
|
+
}: {
|
|
160
|
+
bun_path: string
|
|
161
|
+
entry: string
|
|
162
|
+
board_before: string | null
|
|
163
|
+
cwd: string
|
|
164
|
+
}): Promise<HandOff> => {
|
|
165
|
+
const child = Bun.spawn({
|
|
166
|
+
cmd: [
|
|
167
|
+
bun_path,
|
|
168
|
+
entry,
|
|
169
|
+
'upgrade',
|
|
170
|
+
'--finish',
|
|
171
|
+
...(board_before === null ? [] : ['--board-before', board_before]),
|
|
172
|
+
],
|
|
173
|
+
cwd,
|
|
174
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
175
|
+
})
|
|
176
|
+
return { handed_off: true, exit_code: await child.exited }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The install `bun add -g` just made: found with `findNewInstall`, and
|
|
181
|
+
* handed the last steps with `handOff`. When it isn't found, or can't
|
|
182
|
+
* finish, says why, and this process runs them.
|
|
183
|
+
*/
|
|
184
|
+
const newInstallOf = ({ home }: { home: string }): NewInstall => ({
|
|
185
|
+
finish: async ({ version, board_before }) => {
|
|
186
|
+
const bin = await runCommand({
|
|
187
|
+
cmd: [process.execPath, 'pm', 'bin', '-g'],
|
|
188
|
+
cwd: home,
|
|
189
|
+
timeout_ms: 30_000,
|
|
190
|
+
})
|
|
191
|
+
if (bin.exit_code !== 0) {
|
|
192
|
+
return {
|
|
193
|
+
handed_off: false,
|
|
194
|
+
why: `Bun didn't say where its global commands are (bun pm bin -g exited ${bin.exit_code})`,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const found = await findNewInstall({
|
|
198
|
+
bin_dir: bin.stdout.trim(),
|
|
199
|
+
version,
|
|
200
|
+
})
|
|
201
|
+
if (!found.ok) return { handed_off: false, why: found.why }
|
|
202
|
+
return handOff({
|
|
203
|
+
bun_path: process.execPath,
|
|
204
|
+
entry: found.entry,
|
|
205
|
+
board_before,
|
|
206
|
+
cwd: home,
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
/** What `luca upgrade`'s flags ask for. */
|
|
212
|
+
export type UpgradeArgs =
|
|
213
|
+
/** `luca upgrade [--to <version>]`; `to` is `null` without `--to`. */
|
|
214
|
+
| { finish: false; to: string | null }
|
|
215
|
+
/** `luca upgrade --finish [--board-before <fingerprint>]`, from an older Luca. */
|
|
216
|
+
| { finish: true; board_before: string | null }
|
|
217
|
+
|
|
218
|
+
/** A flag's value: not empty and not another flag. */
|
|
219
|
+
const isValue = (value: string | undefined): value is string =>
|
|
220
|
+
value !== undefined && value !== '' && !value.startsWith('-')
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The flags after `upgrade`, or `undefined` when they're bad. Pure.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* parseUpgradeArgs({ argv: ['--to', '14.0.0-alpha.3'] }) // { finish: false, to: '14.0.0-alpha.3' }
|
|
227
|
+
* parseUpgradeArgs({ argv: ['--finish'] }) // { finish: true, board_before: null }
|
|
228
|
+
*/
|
|
229
|
+
export const parseUpgradeArgs = ({
|
|
230
|
+
argv,
|
|
231
|
+
}: {
|
|
232
|
+
argv: string[]
|
|
233
|
+
}): UpgradeArgs | undefined => {
|
|
234
|
+
const [flag, value, ...rest] = argv
|
|
235
|
+
if (flag === undefined) return { finish: false, to: null }
|
|
236
|
+
if (flag === '--to' && isValue(value) && rest.length === 0) {
|
|
237
|
+
return { finish: false, to: value }
|
|
238
|
+
}
|
|
239
|
+
if (flag === '--finish' && value === undefined) {
|
|
240
|
+
return { finish: true, board_before: null }
|
|
241
|
+
}
|
|
242
|
+
if (flag === '--finish' && value === '--board-before') {
|
|
243
|
+
const [board_before, ...extra] = rest
|
|
244
|
+
if (isValue(board_before) && extra.length === 0) {
|
|
245
|
+
return { finish: true, board_before }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return undefined
|
|
89
249
|
}
|
|
90
250
|
|
|
251
|
+
/** Doctor's computer checks of the Luca on disk now. */
|
|
252
|
+
const computerChecksOf =
|
|
253
|
+
({ home, paseo }: { home: string; paseo: Paseo }) =>
|
|
254
|
+
async () =>
|
|
255
|
+
computerChecks({
|
|
256
|
+
home,
|
|
257
|
+
// Read again: an install changes the version on disk.
|
|
258
|
+
...(await lucaInstall()),
|
|
259
|
+
computer: computerOf({ home }),
|
|
260
|
+
muninn: muninnOf({ home }),
|
|
261
|
+
muninn_health: muninnHealth,
|
|
262
|
+
claude: claudeOf({ home }),
|
|
263
|
+
paseo,
|
|
264
|
+
env: process.env,
|
|
265
|
+
})
|
|
266
|
+
|
|
91
267
|
/** Runs `luca upgrade` with the flags after `upgrade`; returns the exit code. */
|
|
92
268
|
export const upgradeCommand = async ({
|
|
93
269
|
argv,
|
|
94
270
|
}: {
|
|
95
271
|
argv: string[]
|
|
96
272
|
}): Promise<number> => {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
273
|
+
const args = parseUpgradeArgs({ argv })
|
|
274
|
+
if (args === undefined) {
|
|
99
275
|
console.error(UPGRADE_USAGE)
|
|
100
276
|
return 2
|
|
101
277
|
}
|
|
102
278
|
const home = homedir()
|
|
103
279
|
const paseo = paseoOf({ client_id: 'luca-upgrade' })
|
|
280
|
+
const log = (line: string) => {
|
|
281
|
+
console.log(line)
|
|
282
|
+
}
|
|
104
283
|
try {
|
|
105
284
|
const { luca_version, ...install } = await lucaInstall()
|
|
285
|
+
if (args.finish) {
|
|
286
|
+
const end = await finishUpgrade({
|
|
287
|
+
version: luca_version,
|
|
288
|
+
board_before: args.board_before,
|
|
289
|
+
home,
|
|
290
|
+
paseo,
|
|
291
|
+
...install,
|
|
292
|
+
computer_checks: computerChecksOf({ home, paseo }),
|
|
293
|
+
log,
|
|
294
|
+
})
|
|
295
|
+
return end.ok ? 0 : 1
|
|
296
|
+
}
|
|
106
297
|
const end = await runUpgrade({
|
|
107
|
-
to,
|
|
298
|
+
to: args.to,
|
|
299
|
+
home,
|
|
108
300
|
installed_version: luca_version,
|
|
109
301
|
runs_dir: defaultRunsDir(),
|
|
110
302
|
registry_path: defaultRegistryPath({
|
|
111
303
|
env: process.env,
|
|
112
304
|
home_dir: home,
|
|
113
305
|
}),
|
|
306
|
+
list_processes: listProcesses,
|
|
114
307
|
npm: npmOf(),
|
|
115
308
|
bun: bunOf({ home }),
|
|
116
309
|
paseo,
|
|
117
310
|
...install,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Read again: the install changed the version on disk.
|
|
122
|
-
...(await lucaInstall()),
|
|
123
|
-
computer: computerOf({ home }),
|
|
124
|
-
muninn: muninnOf({ home }),
|
|
125
|
-
muninn_health: muninnHealth,
|
|
126
|
-
claude: claudeOf({ home }),
|
|
127
|
-
paseo,
|
|
128
|
-
}),
|
|
129
|
-
log: (line) => {
|
|
130
|
-
console.log(line)
|
|
131
|
-
},
|
|
311
|
+
new_install: newInstallOf({ home }),
|
|
312
|
+
computer_checks: computerChecksOf({ home, paseo }),
|
|
313
|
+
log,
|
|
132
314
|
})
|
|
133
315
|
return end.ok ? 0 : 1
|
|
134
316
|
} catch (error) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { setUpBoard } from './board-in-paseo'
|
|
5
|
+
import type { PaseoPlugins } from './computer-adapters'
|
|
6
|
+
import {
|
|
7
|
+
formatChecks,
|
|
8
|
+
hasProblem,
|
|
9
|
+
reason,
|
|
10
|
+
type DoctorCheck,
|
|
11
|
+
} from './doctor-checks'
|
|
12
|
+
import { installLucaSkills } from './luca-skills'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The last steps of `luca upgrade`, after `bun add -g` (#529): reload the
|
|
16
|
+
* board, say when `/reload-skills` is needed, copy Luca's own skills, and
|
|
17
|
+
* run doctor's computer checks.
|
|
18
|
+
*
|
|
19
|
+
* They run in the newly installed Luca, not the one that installed it:
|
|
20
|
+
* `luca upgrade` hands off to the new install's `luca upgrade --finish`,
|
|
21
|
+
* so the board setup, the skills, and the checks are the new version's.
|
|
22
|
+
*
|
|
23
|
+
* Older Luca looks for this file next to the new install's `luca.ts` to
|
|
24
|
+
* tell whether the new install can finish an upgrade. Don't rename or move
|
|
25
|
+
* it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** How upgrade ended; `message` is the last line it printed. */
|
|
29
|
+
export type UpgradeEnd = { ok: boolean; message: string }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A fingerprint of every file in `dir` but its `node_modules`, or `null`
|
|
33
|
+
* when it can't be read. Short, so it fits on a command line.
|
|
34
|
+
*/
|
|
35
|
+
export const boardFingerprint = async (dir: string): Promise<string | null> => {
|
|
36
|
+
const lines: string[] = []
|
|
37
|
+
const walk = async (at: string): Promise<void> => {
|
|
38
|
+
const entries = await readdir(at, { withFileTypes: true })
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const path = join(at, entry.name)
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
if (entry.name !== 'node_modules') await walk(path)
|
|
43
|
+
} else if (entry.isFile()) {
|
|
44
|
+
const bytes = await Bun.file(path).arrayBuffer()
|
|
45
|
+
lines.push(`${path}\t${Bun.hash(bytes)}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await walk(dir)
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return Bun.hash(lines.sort().join('\n')).toString(36)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Runs `luca upgrade`'s last steps for the installed `version`: reloads
|
|
59
|
+
* the board with its settings kept and its paths rewritten, says to run
|
|
60
|
+
* `/reload-skills` when the board's files differ from `board_before`,
|
|
61
|
+
* copies Luca's skills, then prints `computer_checks`. Ends with "Luca
|
|
62
|
+
* <version> is installed." Never throws.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const end = await finishUpgrade({ version: '14.0.0-alpha.11', board_before, home, paseo, board_dir, skills_dir, engine_path, bun_path, computer_checks, log: console.log })
|
|
66
|
+
*/
|
|
67
|
+
export const finishUpgrade = async ({
|
|
68
|
+
version,
|
|
69
|
+
board_before,
|
|
70
|
+
home,
|
|
71
|
+
paseo,
|
|
72
|
+
board_dir,
|
|
73
|
+
skills_dir,
|
|
74
|
+
engine_path,
|
|
75
|
+
bun_path,
|
|
76
|
+
computer_checks,
|
|
77
|
+
log,
|
|
78
|
+
}: {
|
|
79
|
+
/** The version just installed. */
|
|
80
|
+
version: string
|
|
81
|
+
/** The board folder's fingerprint before the install, or `null` when unknown. */
|
|
82
|
+
board_before: string | null
|
|
83
|
+
/** The home folder, whose `.claude/skills` gets Luca's own skills. */
|
|
84
|
+
home: string
|
|
85
|
+
paseo: PaseoPlugins
|
|
86
|
+
/** The board folder inside Luca's install folder. */
|
|
87
|
+
board_dir: string
|
|
88
|
+
/** The skills folder inside Luca's install folder. */
|
|
89
|
+
skills_dir: string
|
|
90
|
+
/** The real path of the installed `luca-run`. */
|
|
91
|
+
engine_path: string
|
|
92
|
+
/** Bun's own path. */
|
|
93
|
+
bun_path: string
|
|
94
|
+
/** Doctor's computer checks of the new install; none when not given. */
|
|
95
|
+
computer_checks?: () => Promise<DoctorCheck[]>
|
|
96
|
+
log: (line: string) => void
|
|
97
|
+
}): Promise<UpgradeEnd> => {
|
|
98
|
+
const say = ({ message, ok }: UpgradeEnd): UpgradeEnd => {
|
|
99
|
+
log(`[luca upgrade] ${message}`)
|
|
100
|
+
return { ok, message }
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const board = await setUpBoard({
|
|
104
|
+
command: 'luca upgrade',
|
|
105
|
+
paseo,
|
|
106
|
+
// Upgrade only reloads; `luca init` asks to turn plugins on.
|
|
107
|
+
ask: async () => false,
|
|
108
|
+
board_dir,
|
|
109
|
+
engine_path,
|
|
110
|
+
bun_path,
|
|
111
|
+
log,
|
|
112
|
+
})
|
|
113
|
+
if (board.ok) {
|
|
114
|
+
const after = await boardFingerprint(board_dir)
|
|
115
|
+
if (
|
|
116
|
+
board_before === null ||
|
|
117
|
+
after === null ||
|
|
118
|
+
board_before !== after
|
|
119
|
+
) {
|
|
120
|
+
log(
|
|
121
|
+
"[luca upgrade] The board's files changed: run /reload-skills in a Paseo chat so its slash commands are up to date."
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const skills = await installLucaSkills({
|
|
126
|
+
home,
|
|
127
|
+
skills_dir,
|
|
128
|
+
prefix: '[luca upgrade]',
|
|
129
|
+
log,
|
|
130
|
+
})
|
|
131
|
+
const end = say({
|
|
132
|
+
message: !board.ok
|
|
133
|
+
? `Luca ${version} is installed, but the board wasn't reloaded. Run luca init to fix it.`
|
|
134
|
+
: !skills.ok
|
|
135
|
+
? `Luca ${version} is installed, but its skills weren't copied. Run luca doctor --fix to fix it.`
|
|
136
|
+
: `Luca ${version} is installed.`,
|
|
137
|
+
ok: board.ok && skills.ok,
|
|
138
|
+
})
|
|
139
|
+
const checks = (await computer_checks?.()) ?? []
|
|
140
|
+
for (const line of formatChecks({ checks })) log(line)
|
|
141
|
+
return { ...end, ok: end.ok && !hasProblem({ checks }) }
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Such as computer checks that threw.
|
|
144
|
+
return say({
|
|
145
|
+
message: `Luca ${version} is installed, but its last steps failed: ${reason(error)}`,
|
|
146
|
+
ok: false,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|