@alecsibilia/luca 14.0.0-alpha.0 → 14.0.0-alpha.1
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 +6 -0
- package/README.md +5 -7
- package/board/index.server.ts +4 -29
- package/board/package.json +1 -1
- package/board/server/luca-version.ts +2 -0
- package/board/server/reduce-board.ts +1 -0
- package/board/shared/board-rpc.ts +2 -2
- package/engine/agents/role-prompts.ts +17 -0
- package/engine/cli/doctor-command.ts +1 -13
- package/engine/cli/init-command.ts +33 -1
- package/engine/cli/init.ts +27 -6
- package/engine/cli/luca.ts +3 -2
- package/engine/cli/repo-adapters-real.ts +19 -2
- package/engine/cli/setup.ts +24 -3
- package/engine/config/engine-config.ts +6 -0
- package/engine/core/changeset.ts +6 -7
- package/engine/core/decide-build.ts +51 -1
- package/engine/core/execute-build.ts +31 -2
- package/engine/core/execute-changeset.ts +23 -15
- package/engine/core/stuck-text.ts +4 -0
- package/engine/gates/gate-runner.ts +27 -1
- package/engine/gates/gate-schemas.ts +9 -2
- package/engine/journal/journal-record.ts +18 -2
- package/engine/journal/replay.ts +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @alecsibilia/luca
|
|
2
2
|
|
|
3
|
+
## 14.0.0-alpha.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8a58510: Spec: Luca v14 fixes: no empty changesets, board version, init sets up the repo ([#480](https://github.com/asibilia/luca-framework/issues/480))
|
|
8
|
+
|
|
3
9
|
## 14.0.0-alpha.0
|
|
4
10
|
|
|
5
11
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -24,17 +24,15 @@ This is Luca v14, in alpha. Old Luca (v13) is still on npm; see [Coming from v13
|
|
|
24
24
|
|
|
25
25
|
While v14 is in alpha, npm's `latest` is still v13, so install `@alecsibilia/luca@alpha` for now.
|
|
26
26
|
|
|
27
|
-
2. Set up this computer, once:
|
|
27
|
+
2. Set up this computer and your repo, once, from inside your repo:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
luca init
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Run inside a git repo, `luca init` sets up this computer, then runs `luca setup` to set up that repo too. Run outside a repo, it sets up only this computer. For another repo later, run `luca setup` from inside it.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
luca setup
|
|
37
|
-
```
|
|
35
|
+
3. Check what `luca setup` wrote to `.luca/config.json`. It names your repo's GitHub name (like `tmnb`) as its memory vault in `muninn.vault`; change it if you want another.
|
|
38
36
|
|
|
39
37
|
4. In Claude Code, in your repo, run `/setup-matt-pocock-skills` so the planning skills know your repo.
|
|
40
38
|
5. Write a spec with `/to-spec`, then split it into tickets with `/to-tickets`.
|
|
@@ -44,8 +42,8 @@ This is Luca v14, in alpha. Old Luca (v13) is still on npm; see [Coming from v13
|
|
|
44
42
|
|
|
45
43
|
| Command | What it does |
|
|
46
44
|
| ------- | ------------ |
|
|
47
|
-
| `luca init` | Sets up this computer, once (safe to run again): MuninnDB and its Claude Code entry, the board in Paseo with its engine and Bun paths, and the planning skills (`to-spec`, `to-tickets`, `setup-matt-pocock-skills`, `grilling`, `domain-modeling`). It asks before turning on Paseo's plugins, and leaves skills you already have alone. `--skip-skills` skips the skills. |
|
|
48
|
-
| `luca setup` | Gets the repo in this folder ready: the labels a run needs (and the `release:*` labels when the repo uses changesets), `.luca/config.json
|
|
45
|
+
| `luca init` | Sets up this computer, once (safe to run again): MuninnDB and its Claude Code entry, the board in Paseo with its engine and Bun paths, and the planning skills (`to-spec`, `to-tickets`, `setup-matt-pocock-skills`, `grilling`, `domain-modeling`). It asks before turning on Paseo's plugins, and leaves skills you already have alone. `--skip-skills` skips the skills. Inside a git repo, it then runs `luca setup` for that repo. |
|
|
46
|
+
| `luca setup` | Gets the repo in this folder ready: the labels a run needs (and the `release:*` labels when the repo uses changesets), `.luca/config.json` (a new one gets the repo's GitHub name as its vault; an existing vault is kept), and a check of GitHub sub-issues and dependencies. `--base <branch>` names the base branch. |
|
|
49
47
|
| `luca doctor` | Checks this computer, this repo when you're in one, and what v13 left behind. Each problem comes with its exact fix. `luca doctor --fix` fixes what's safe without asking: it never deletes (v13 files go to a dated backup folder) and never commits, and it lists the repo files to commit. |
|
|
50
48
|
| `luca upgrade` | Moves to the newest Luca on your channel: `alpha` stays on `alpha`, and it never goes back to v13. It refuses while any run is going (stuck runs and limit waits count) and lists them. Then it reloads the board and says when to run `/reload-skills`. |
|
|
51
49
|
| `luca upgrade --to <version>` | Installs that exact version, an older one too, to go back when a version breaks. |
|
package/board/index.server.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
3
|
|
|
6
4
|
import type {
|
|
7
5
|
PluginHandlerContext,
|
|
@@ -11,6 +9,7 @@ import type {
|
|
|
11
9
|
import { createBoardServer } from './server/board-server'
|
|
12
10
|
import { ENGINE_CHECK_MS } from './server/engine-watch'
|
|
13
11
|
import { listProcesses } from './server/list-processes'
|
|
12
|
+
import { LUCA_VERSION } from './server/luca-version'
|
|
14
13
|
import { runCommand } from './server/run-command'
|
|
15
14
|
import { defaultRunsDir } from './server/run-journals'
|
|
16
15
|
import { defaultRegistryPath } from './server/run-registry'
|
|
@@ -37,29 +36,6 @@ type Paseo = PluginHandlerContext['paseo']
|
|
|
37
36
|
|
|
38
37
|
const log = (message: string) => console.error(`[${PLUGIN_ID}] ${message}`)
|
|
39
38
|
|
|
40
|
-
/**
|
|
41
|
-
* The Luca version in the board folder's package.json, which the published
|
|
42
|
-
* package stamps with its own version; `null` when it can't be read.
|
|
43
|
-
*/
|
|
44
|
-
const loadedVersion = (): string | null => {
|
|
45
|
-
try {
|
|
46
|
-
const manifest: unknown = JSON.parse(
|
|
47
|
-
readFileSync(
|
|
48
|
-
join(dirname(fileURLToPath(import.meta.url)), 'package.json'),
|
|
49
|
-
'utf8'
|
|
50
|
-
)
|
|
51
|
-
)
|
|
52
|
-
return typeof manifest === 'object' &&
|
|
53
|
-
manifest !== null &&
|
|
54
|
-
'version' in manifest &&
|
|
55
|
-
typeof manifest.version === 'string'
|
|
56
|
-
? manifest.version
|
|
57
|
-
: null
|
|
58
|
-
} catch {
|
|
59
|
-
return null
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
39
|
/**
|
|
64
40
|
* The board plugin's daemon side: the engine settings (their usage lines
|
|
65
41
|
* kept in a file for the engine), `run.start` (launch a
|
|
@@ -154,9 +130,8 @@ export default function contribute(server: PluginServerContext) {
|
|
|
154
130
|
connect({ context })
|
|
155
131
|
return board.readBoard(input)
|
|
156
132
|
})
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
server.handle(boardVersionRpc, () => ({ version }))
|
|
133
|
+
// Fixed as loaded: after an upgrade it differs until a reload.
|
|
134
|
+
server.handle(boardVersionRpc, () => ({ version: LUCA_VERSION }))
|
|
160
135
|
|
|
161
136
|
// Lifecycle hooks time out at 30 s, so the first check is not awaited.
|
|
162
137
|
void board.checkEngines()
|
package/board/package.json
CHANGED
|
@@ -49,6 +49,7 @@ const STUCK_REASONS: Record<string, string> = {
|
|
|
49
49
|
join_failed: "The ticket couldn't join the run branch.",
|
|
50
50
|
join_gates_failed: 'The checks failed after the ticket joined.',
|
|
51
51
|
install_failed: 'Installing the dependencies failed.',
|
|
52
|
+
prepare_failed: "The repo's prepare command failed.",
|
|
52
53
|
setup_change_needed:
|
|
53
54
|
'An agent needs a test setup file changed; only you may change one.',
|
|
54
55
|
crashed: 'The engine crashed in the same step again and again.',
|
|
@@ -117,8 +117,8 @@ export type BoardReadInput = z.input<typeof BoardReadInputSchema>
|
|
|
117
117
|
export type BoardReadOutput = z.infer<typeof BoardReadOutputSchema>
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* API Response: the Luca version of the loaded board,
|
|
121
|
-
*
|
|
120
|
+
* API Response: the Luca version of the loaded board, from its version
|
|
121
|
+
* module (the dev value from the repo's source); `null` from older boards.
|
|
122
122
|
*/
|
|
123
123
|
export const BoardVersionOutputSchema = z.object({
|
|
124
124
|
version: z.string().nullable(),
|
|
@@ -109,6 +109,23 @@ export const runNotesSection = ({
|
|
|
109
109
|
.join('\n'),
|
|
110
110
|
]
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The section a test-writer or implementer gets when the repo has a prepare
|
|
114
|
+
* command: the engine runs it before its test runs, so tests that need its
|
|
115
|
+
* outputs may fail when the agent runs them itself.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* prepareSection({ command: 'bun run build:rom' })
|
|
119
|
+
* // '## This repo has a prepare command\n\n...'
|
|
120
|
+
*/
|
|
121
|
+
export const prepareSection = ({ command }: { command: string }): string =>
|
|
122
|
+
[
|
|
123
|
+
'## This repo has a prepare command',
|
|
124
|
+
`The engine runs \`${command}\` in the worktree before each of its test runs (the red check and the gates), so tests that need its outputs find them up to date. ` +
|
|
125
|
+
"Never run it yourself. Tests that need its outputs may fail when you run them, and still pass at the engine's checks. " +
|
|
126
|
+
"If the engine's prepare command fails after your turn, its output comes back to you like a failed check.",
|
|
127
|
+
].join('\n\n')
|
|
128
|
+
|
|
112
129
|
/** What each lens judges the whole run branch on, in one paragraph. */
|
|
113
130
|
const LENS_TASK_TEXT =
|
|
114
131
|
'You are one lens of the final review: a fresh, read-only reviewer of the WHOLE run branch, every ticket of the spec together. Write nothing.'
|
|
@@ -24,25 +24,13 @@ import {
|
|
|
24
24
|
paseoOf,
|
|
25
25
|
} from './computer-adapters-real'
|
|
26
26
|
import { runDoctor } from './doctor'
|
|
27
|
-
import { githubOf, memoryOf } from './repo-adapters-real'
|
|
27
|
+
import { githubOf, memoryOf, repoTop } from './repo-adapters-real'
|
|
28
28
|
|
|
29
29
|
import type { MemoryClient } from '../memory/memory-client'
|
|
30
|
-
import { runCommand } from '../shell/run-command'
|
|
31
30
|
|
|
32
31
|
/** `luca doctor`'s usage line. */
|
|
33
32
|
export const DOCTOR_USAGE = 'Usage: luca doctor [--fix]'
|
|
34
33
|
|
|
35
|
-
/** The top of the git repo `cwd` is in, or `null` outside one. */
|
|
36
|
-
const repoTop = async ({ cwd }: { cwd: string }): Promise<string | null> => {
|
|
37
|
-
const top = await runCommand({
|
|
38
|
-
cmd: ['git', 'rev-parse', '--show-toplevel'],
|
|
39
|
-
cwd,
|
|
40
|
-
})
|
|
41
|
-
return top.exit_code === 0 && top.stdout.trim() !== ''
|
|
42
|
-
? top.stdout.trim()
|
|
43
|
-
: null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
34
|
/** Runs `luca doctor` with the flags after `doctor`; returns the exit code. */
|
|
47
35
|
export const doctorCommand = async ({
|
|
48
36
|
argv,
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* all of that: runs then have memory off. It puts the board into Paseo from
|
|
10
10
|
* Luca's own install folder and writes its engine and Bun paths, asking
|
|
11
11
|
* before it turns Paseo's plugins on. It installs the planning skills that
|
|
12
|
-
* aren't there yet; `--skip-skills` skips them.
|
|
12
|
+
* aren't there yet; `--skip-skills` skips them. Inside a git repo, it then
|
|
13
|
+
* runs `luca setup` for that repo. See `runInit`.
|
|
13
14
|
*
|
|
14
15
|
* Exits 0 when done, 1 when a step failed, 2 on bad flags.
|
|
15
16
|
*/
|
|
@@ -29,7 +30,9 @@ import {
|
|
|
29
30
|
} from './computer-adapters-real'
|
|
30
31
|
import { reason } from './doctor-checks'
|
|
31
32
|
import { runInit, type Launchctl, type SkillsTool } from './init'
|
|
33
|
+
import { githubOf, memoryOf, repoTop } from './repo-adapters-real'
|
|
32
34
|
|
|
35
|
+
import type { MemoryClient } from '../memory/memory-client'
|
|
33
36
|
import { runCommand } from '../shell/run-command'
|
|
34
37
|
|
|
35
38
|
/** `luca init`'s flags. */
|
|
@@ -103,6 +106,24 @@ const skillsOf = ({ home }: { home: string }): SkillsTool => ({
|
|
|
103
106
|
},
|
|
104
107
|
})
|
|
105
108
|
|
|
109
|
+
/**
|
|
110
|
+
* MuninnDB over MCP, found on first use: init writes Claude Code's `muninn`
|
|
111
|
+
* entry before its repo part reads it.
|
|
112
|
+
*/
|
|
113
|
+
const lazyMemory = (): MemoryClient => {
|
|
114
|
+
let client: Promise<MemoryClient> | null = null
|
|
115
|
+
const found = () => (client ??= memoryOf())
|
|
116
|
+
return {
|
|
117
|
+
recall: async (args) => (await found()).recall(args),
|
|
118
|
+
remember: async (args) => (await found()).remember(args),
|
|
119
|
+
evolve: async (args) => (await found()).evolve(args),
|
|
120
|
+
feedback: async (args) => (await found()).feedback(args),
|
|
121
|
+
close: async () => {
|
|
122
|
+
if (client !== null) await (await client).close()
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
/** Runs `luca init` with the flags after `init`; returns the exit code. */
|
|
107
128
|
export const initCommand = async ({
|
|
108
129
|
argv,
|
|
@@ -115,11 +136,21 @@ export const initCommand = async ({
|
|
|
115
136
|
}
|
|
116
137
|
const home = homedir()
|
|
117
138
|
const paseo = paseoOf({ client_id: 'luca-init' })
|
|
139
|
+
const memory = lazyMemory()
|
|
118
140
|
try {
|
|
141
|
+
const top = await repoTop({ cwd: process.cwd() })
|
|
119
142
|
const end = await runInit({
|
|
120
143
|
home,
|
|
121
144
|
skip_muninndb: argv.includes('--skip-muninndb'),
|
|
122
145
|
skip_skills: argv.includes('--skip-skills'),
|
|
146
|
+
repo:
|
|
147
|
+
top === null
|
|
148
|
+
? null
|
|
149
|
+
: {
|
|
150
|
+
path: top,
|
|
151
|
+
github: await githubOf({ cwd: top }),
|
|
152
|
+
memory,
|
|
153
|
+
},
|
|
123
154
|
muninn: muninnOf({ home }),
|
|
124
155
|
muninn_health: muninnHealth,
|
|
125
156
|
claude: claudeOf({ home }),
|
|
@@ -139,5 +170,6 @@ export const initCommand = async ({
|
|
|
139
170
|
return 1
|
|
140
171
|
} finally {
|
|
141
172
|
await paseo.close()
|
|
173
|
+
await memory.close().catch(() => undefined)
|
|
142
174
|
}
|
|
143
175
|
}
|
package/engine/cli/init.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
Paseo,
|
|
13
13
|
} from './computer-adapters'
|
|
14
14
|
import { computerChecks } from './computer-checks'
|
|
15
|
+
import type { DoctorRepo } from './doctor'
|
|
15
16
|
import {
|
|
16
17
|
formatChecks,
|
|
17
18
|
hasProblem,
|
|
@@ -19,11 +20,12 @@ import {
|
|
|
19
20
|
type DoctorCheck,
|
|
20
21
|
} from './doctor-checks'
|
|
21
22
|
import { ensureMuninnEntry, hideToken } from './muninn-entry'
|
|
23
|
+
import { runSetup } from './setup'
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* `luca init`: sets up this computer for Luca, once, and is safe to run
|
|
25
27
|
* again. It has three parts, each run even when an earlier one failed, and
|
|
26
|
-
* ends with doctor's computer checks.
|
|
28
|
+
* ends with doctor's computer checks, then sets up the repo it runs in.
|
|
27
29
|
*
|
|
28
30
|
* Memory:
|
|
29
31
|
*
|
|
@@ -48,6 +50,10 @@ import { ensureMuninnEntry, hideToken } from './muninn-entry'
|
|
|
48
50
|
* `mattpocock/skills` with the `skills` tool, skipping any already there.
|
|
49
51
|
* `skip_skills` skips this part.
|
|
50
52
|
*
|
|
53
|
+
* The repo: inside a git repo, after the computer's checks, it runs `luca
|
|
54
|
+
* setup` for that repo (see `runSetup`), with the output setup gives on its
|
|
55
|
+
* own. Outside one, it stops after the computer part.
|
|
56
|
+
*
|
|
51
57
|
* MuninnDB's CLI, `claude mcp`, `launchctl`, Paseo, the question to the
|
|
52
58
|
* user, the `skills` tool, and the computer's tools are adapters, so tests
|
|
53
59
|
* use fakes. The token is never printed, returned, or thrown.
|
|
@@ -258,17 +264,19 @@ const setUpMemory = async ({
|
|
|
258
264
|
|
|
259
265
|
/**
|
|
260
266
|
* Runs `luca init` against `home` with these adapters: memory, then the
|
|
261
|
-
* board, then the planning skills, then doctor's computer checks,
|
|
262
|
-
*
|
|
263
|
-
* `ok: false`, as does a check that is
|
|
267
|
+
* board, then the planning skills, then doctor's computer checks, then
|
|
268
|
+
* `luca setup` in `repo` when there is one. Never throws: a failure is
|
|
269
|
+
* logged (token hidden) and ends with `ok: false`, as does a check that is
|
|
270
|
+
* a problem or a setup to-do.
|
|
264
271
|
*
|
|
265
272
|
* @example
|
|
266
|
-
* const end = await runInit({ home: homedir(), skip_muninndb: false, skip_skills: false, muninn, muninn_health, claude, launchctl, paseo, skills, ask, computer, ...(await lucaInstall()), log: console.log })
|
|
273
|
+
* const end = await runInit({ home: homedir(), skip_muninndb: false, skip_skills: false, repo: null, muninn, muninn_health, claude, launchctl, paseo, skills, ask, computer, ...(await lucaInstall()), log: console.log })
|
|
267
274
|
*/
|
|
268
275
|
export const runInit = async ({
|
|
269
276
|
home,
|
|
270
277
|
skip_muninndb,
|
|
271
278
|
skip_skills,
|
|
279
|
+
repo = null,
|
|
272
280
|
muninn,
|
|
273
281
|
muninn_health,
|
|
274
282
|
claude,
|
|
@@ -286,6 +294,8 @@ export const runInit = async ({
|
|
|
286
294
|
home: string
|
|
287
295
|
skip_muninndb: boolean
|
|
288
296
|
skip_skills: boolean
|
|
297
|
+
/** The git repo init runs in, set up after the computer; `null` outside one. */
|
|
298
|
+
repo?: DoctorRepo | null
|
|
289
299
|
muninn: MuninnCli
|
|
290
300
|
muninn_health: MuninnHealth
|
|
291
301
|
claude: ClaudeMcp
|
|
@@ -333,12 +343,23 @@ export const runInit = async ({
|
|
|
333
343
|
paseo,
|
|
334
344
|
})
|
|
335
345
|
for (const line of formatChecks({ checks: doctor })) log(line)
|
|
346
|
+
const setup =
|
|
347
|
+
repo === null
|
|
348
|
+
? null
|
|
349
|
+
: await runSetup({
|
|
350
|
+
repo: repo.path,
|
|
351
|
+
github: repo.github,
|
|
352
|
+
memory: repo.memory,
|
|
353
|
+
base_branch: repo.base_branch,
|
|
354
|
+
log,
|
|
355
|
+
})
|
|
336
356
|
return {
|
|
337
357
|
ok:
|
|
338
358
|
memory.ok &&
|
|
339
359
|
board.ok &&
|
|
340
360
|
planning.ok &&
|
|
341
|
-
!hasProblem({ checks: doctor })
|
|
361
|
+
!hasProblem({ checks: doctor }) &&
|
|
362
|
+
(setup?.ok ?? true),
|
|
342
363
|
memory: memory.memory,
|
|
343
364
|
message: planning.message,
|
|
344
365
|
doctor,
|
package/engine/cli/luca.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* `luca`: the command people run, with subcommands:
|
|
4
4
|
*
|
|
5
5
|
* luca init [--skip-muninndb] [--skip-skills]
|
|
6
|
-
* sets up this computer, once
|
|
6
|
+
* sets up this computer, once, then
|
|
7
|
+
* the repo in this folder
|
|
7
8
|
* luca setup [--base <branch>] gets the repo in this folder ready
|
|
8
9
|
* luca upgrade [--to <version>] moves to another version of Luca
|
|
9
10
|
* luca doctor [--fix] checks this computer and repo; --fix
|
|
@@ -22,7 +23,7 @@ const USAGE = `Usage: luca <command>
|
|
|
22
23
|
|
|
23
24
|
Commands:
|
|
24
25
|
init [--skip-muninndb] [--skip-skills]
|
|
25
|
-
Set up this computer for Luca (once)
|
|
26
|
+
Set up this computer for Luca (once), then the repo you're in
|
|
26
27
|
setup [--base <branch>] Get the repo in this folder ready for Luca
|
|
27
28
|
upgrade [--to <version>] Move to the newest Luca on your channel, or to <version>
|
|
28
29
|
doctor [--fix] Check this computer and repo; --fix fixes what's safe`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The real repo adapters `luca setup` and `luca doctor`
|
|
3
|
-
* GitHub side, and MuninnDB over MCP for
|
|
2
|
+
* The real repo adapters `luca setup`, `luca init`, and `luca doctor`
|
|
3
|
+
* share: the repo's top folder, its GitHub side, and MuninnDB over MCP for
|
|
4
|
+
* its vault.
|
|
4
5
|
*/
|
|
5
6
|
import type { SetupGitHub } from './setup'
|
|
6
7
|
|
|
@@ -10,12 +11,28 @@ import {
|
|
|
10
11
|
createMuninnMcpClient,
|
|
11
12
|
muninnSettings,
|
|
12
13
|
} from '../memory/muninn-mcp-client'
|
|
14
|
+
import { runCommand } from '../shell/run-command'
|
|
13
15
|
import {
|
|
14
16
|
createGitHubTracker,
|
|
15
17
|
ghLogin,
|
|
16
18
|
githubRepoOf,
|
|
17
19
|
} from '../tracker/github-tracker'
|
|
18
20
|
|
|
21
|
+
/** The top of the git repo `cwd` is in, or `null` outside one. */
|
|
22
|
+
export const repoTop = async ({
|
|
23
|
+
cwd,
|
|
24
|
+
}: {
|
|
25
|
+
cwd: string
|
|
26
|
+
}): Promise<string | null> => {
|
|
27
|
+
const top = await runCommand({
|
|
28
|
+
cmd: ['git', 'rev-parse', '--show-toplevel'],
|
|
29
|
+
cwd,
|
|
30
|
+
})
|
|
31
|
+
return top.exit_code === 0 && top.stdout.trim() !== ''
|
|
32
|
+
? top.stdout.trim()
|
|
33
|
+
: null
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
/**
|
|
20
37
|
* The real GitHub side: the GitHub tracker of the repo at `cwd` for its
|
|
21
38
|
* labels and issue links, and `gh` for the login. With no GitHub repo, the
|
package/engine/cli/setup.ts
CHANGED
|
@@ -35,7 +35,8 @@ import {
|
|
|
35
35
|
/**
|
|
36
36
|
* `luca setup`: gets a repo ready for Luca. It creates the labels a run
|
|
37
37
|
* needs (and the `release:*` ones in a repo with changesets), writes a
|
|
38
|
-
* starting `.luca/config.json`
|
|
38
|
+
* starting `.luca/config.json` with the repo's GitHub name as its vault
|
|
39
|
+
* (or converts old Luca's, or leaves a
|
|
39
40
|
* new-style one alone), runs `luca doctor`'s repo checks once, and ends
|
|
40
41
|
* with a plain list of what's done and what's left, then a pointer to
|
|
41
42
|
* `/setup-matt-pocock-skills`. It never commits, and running it again
|
|
@@ -262,6 +263,18 @@ const oldVault = ({ raw }: { raw: Record<string, unknown> }): string | null =>
|
|
|
262
263
|
vaultName(isRecord(raw.muninn) ? raw.muninn.vault : undefined) ??
|
|
263
264
|
vaultName(raw.vault)
|
|
264
265
|
|
|
266
|
+
/**
|
|
267
|
+
* The `name` part of a GitHub `owner/name`, a new repo's vault. Pure.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* repoName({ github_repo: 'asibilia/tmnb' }) // 'tmnb'
|
|
271
|
+
*/
|
|
272
|
+
const repoName = ({
|
|
273
|
+
github_repo,
|
|
274
|
+
}: {
|
|
275
|
+
github_repo: string | null
|
|
276
|
+
}): string | null => vaultName(github_repo?.split('/').at(-1)?.trim() ?? null)
|
|
277
|
+
|
|
265
278
|
const writeConfig = async ({
|
|
266
279
|
repo,
|
|
267
280
|
config,
|
|
@@ -591,7 +604,7 @@ const list = (lines: string[]): string =>
|
|
|
591
604
|
* `refactor`, and `needs-info` labels, and in a repo with
|
|
592
605
|
* `.changeset/config.json` the `release:*` ones too; writes a starting
|
|
593
606
|
* `.luca/config.json` from `package.json` when there is none (see
|
|
594
|
-
* `guessChecks`), rewrites an old-Luca config into the new shape keeping
|
|
607
|
+
* `guessChecks`), with the repo's GitHub name as its `muninn.vault`, rewrites an old-Luca config into the new shape keeping
|
|
595
608
|
* its `muninn.vault` (or an older top-level `vault`), and leaves a
|
|
596
609
|
* new-style config alone; then checks the `gh` login and runs `luca
|
|
597
610
|
* doctor`'s repo checks (`repoChecks`) once: the GitHub remote, sub-issues
|
|
@@ -640,7 +653,10 @@ export const runSetup = async ({
|
|
|
640
653
|
|
|
641
654
|
const found = await findConfig({ repo })
|
|
642
655
|
if (found.kind === 'missing' || found.kind === 'old') {
|
|
643
|
-
const vault =
|
|
656
|
+
const vault =
|
|
657
|
+
found.kind === 'old'
|
|
658
|
+
? oldVault({ raw: found.raw })
|
|
659
|
+
: repoName({ github_repo })
|
|
644
660
|
await writeConfig({
|
|
645
661
|
repo,
|
|
646
662
|
config: startingConfig({
|
|
@@ -653,6 +669,11 @@ export const runSetup = async ({
|
|
|
653
669
|
? `Wrote ${ENGINE_CONFIG_FILE} from package.json's scripts. Check it, then merge it to main through a PR.`
|
|
654
670
|
: `Rewrote old Luca's ${ENGINE_CONFIG_FILE} into the new shape${vault === null ? '' : `, keeping the \`${vault}\` vault`}. Check it, then merge it to main through a PR.`
|
|
655
671
|
)
|
|
672
|
+
if (found.kind === 'missing' && vault !== null) {
|
|
673
|
+
done_lines.push(
|
|
674
|
+
`Set the memory vault to \`${vault}\`, the repo's GitHub name. Change \`muninn.vault\` in ${ENGINE_CONFIG_FILE} to use another.`
|
|
675
|
+
)
|
|
676
|
+
}
|
|
656
677
|
} else if (found.kind === 'new') {
|
|
657
678
|
done_lines.push(
|
|
658
679
|
`${ENGINE_CONFIG_FILE} is new-style, so it was left as it is`
|
|
@@ -54,6 +54,12 @@ export const EngineConfigSchema = z.object({
|
|
|
54
54
|
lint: z.string().min(1).optional(),
|
|
55
55
|
})
|
|
56
56
|
.default({}),
|
|
57
|
+
/**
|
|
58
|
+
* A command the engine runs in a checkout before every test run there
|
|
59
|
+
* (baseline, red check, gates), for repos whose tests need build outputs
|
|
60
|
+
* that are gitignored, such as `bun run build:rom`. Agents never run it.
|
|
61
|
+
*/
|
|
62
|
+
prepare: z.string().min(1).optional(),
|
|
57
63
|
test_file_patterns: z.array(z.string()).default(['**/*.test.ts']),
|
|
58
64
|
test_setup_files: z.array(z.string()).default([]),
|
|
59
65
|
rule_files: z.array(z.string()).default([]),
|
package/engine/core/changeset.ts
CHANGED
|
@@ -116,8 +116,8 @@ export const changedPackages = ({
|
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
118
|
* A changeset's text: front matter naming each package with the bump, then
|
|
119
|
-
* the summary. `release:none`
|
|
120
|
-
*
|
|
119
|
+
* the summary. `release:none` gets no changeset at all (#476), so it has no
|
|
120
|
+
* text.
|
|
121
121
|
*
|
|
122
122
|
* @example
|
|
123
123
|
* changesetText({ packages: ['@acme/a'], bump: 'minor', summary: 'Add a' })
|
|
@@ -129,12 +129,11 @@ export const changesetText = ({
|
|
|
129
129
|
summary,
|
|
130
130
|
}: {
|
|
131
131
|
packages: string[]
|
|
132
|
-
bump: ChangesetBump
|
|
132
|
+
bump: Exclude<ChangesetBump, 'none'>
|
|
133
133
|
summary: string
|
|
134
134
|
}): string => {
|
|
135
|
-
const releases =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
: packages.map((name) => `${JSON.stringify(name)}: ${bump}\n`)
|
|
135
|
+
const releases = packages.map(
|
|
136
|
+
(name) => `${JSON.stringify(name)}: ${bump}\n`
|
|
137
|
+
)
|
|
139
138
|
return `---\n${releases.join('')}---\n\n${summary}\n`
|
|
140
139
|
}
|
|
@@ -37,7 +37,11 @@ import {
|
|
|
37
37
|
} from './review-text'
|
|
38
38
|
import { retrySection, setupChangeDetail } from './stuck-text'
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
prepareSection,
|
|
42
|
+
rejoinSection,
|
|
43
|
+
rolePrompt,
|
|
44
|
+
} from '../agents/role-prompts'
|
|
41
45
|
import type { AgentRole, CriterionTests } from '../agents/role-results'
|
|
42
46
|
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
43
47
|
import type {
|
|
@@ -970,6 +974,14 @@ const nextTicketStep = ({
|
|
|
970
974
|
detail: ticketFailure,
|
|
971
975
|
})
|
|
972
976
|
}
|
|
977
|
+
const { prepare } = progress
|
|
978
|
+
if (prepare !== null && !prepare.ok) {
|
|
979
|
+
return stuck({
|
|
980
|
+
ticket: number,
|
|
981
|
+
reason: 'prepare_failed',
|
|
982
|
+
detail: `\`${prepare.command}\` failed in the worktree of #${number} (exit ${prepare.exit_code ?? 'none'}) before any agent worked on it. Agents never run the prepare command, so fix what it needs (a missing tool, say) and retry.\n${prepare.output}`,
|
|
983
|
+
})
|
|
984
|
+
}
|
|
973
985
|
if (progress.baseline === null) return baseline_step
|
|
974
986
|
if (progress.commits.green === null) {
|
|
975
987
|
const rejoin = rejoinStep({
|
|
@@ -1058,6 +1070,44 @@ export const decideBuild = ({
|
|
|
1058
1070
|
}: {
|
|
1059
1071
|
state: RunState
|
|
1060
1072
|
spec_number: number
|
|
1073
|
+
}): BuildAction[] => {
|
|
1074
|
+
const steps = buildSteps({ state, spec_number })
|
|
1075
|
+
const command = state.config?.prepare
|
|
1076
|
+
return command === undefined
|
|
1077
|
+
? steps
|
|
1078
|
+
: steps.map((step) => withPrepareSection({ step, command }))
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* A fresh test-writer's or implementer's launch (a ticket's or the final
|
|
1083
|
+
* review's fixers), with the repo's prepare command named in its prompt.
|
|
1084
|
+
* Any other step as it is.
|
|
1085
|
+
*/
|
|
1086
|
+
const withPrepareSection = ({
|
|
1087
|
+
step,
|
|
1088
|
+
command,
|
|
1089
|
+
}: {
|
|
1090
|
+
step: BuildAction
|
|
1091
|
+
command: string
|
|
1092
|
+
}): BuildAction => {
|
|
1093
|
+
const writesCode =
|
|
1094
|
+
(step.type === 'launch_agent' || step.type === 'launch_final_fixer') &&
|
|
1095
|
+
(step.role === 'test-writer' || step.role === 'implementer')
|
|
1096
|
+
return writesCode
|
|
1097
|
+
? {
|
|
1098
|
+
...step,
|
|
1099
|
+
prompt: `${step.prompt}\n\n${prepareSection({ command })}`,
|
|
1100
|
+
}
|
|
1101
|
+
: step
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** `decideBuild`'s steps, before the prepare command joins any prompt. */
|
|
1105
|
+
const buildSteps = ({
|
|
1106
|
+
state,
|
|
1107
|
+
spec_number,
|
|
1108
|
+
}: {
|
|
1109
|
+
state: RunState
|
|
1110
|
+
spec_number: number
|
|
1061
1111
|
}): BuildAction[] => {
|
|
1062
1112
|
const { snapshot, run_branch, run_branch_install, pull_request } = state
|
|
1063
1113
|
const base_branch = state.base_branch ?? 'main'
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type RoleResult,
|
|
18
18
|
} from '../agents/role-results'
|
|
19
19
|
import { bunTestCommands, type EngineConfig } from '../config/engine-config'
|
|
20
|
-
import { runGates, shellCheck } from '../gates/gate-runner'
|
|
20
|
+
import { prepareCheck, runGates, shellCheck } from '../gates/gate-runner'
|
|
21
21
|
import { newCodeFiles, importStem, scanLeftovers } from '../gates/leftover-scan'
|
|
22
22
|
import {
|
|
23
23
|
dependenciesChanged,
|
|
@@ -155,6 +155,7 @@ const runRedCheck = async ({
|
|
|
155
155
|
what: `baseline test run for #${action.ticket}`,
|
|
156
156
|
})
|
|
157
157
|
const test_files = await testFilesIn({ context, cwd: path })
|
|
158
|
+
const prepare = await prepareCheck({ config: context.config, cwd: path })
|
|
158
159
|
const current = await runBunTests({
|
|
159
160
|
cwd: path,
|
|
160
161
|
commands: bunTestCommands(context),
|
|
@@ -170,7 +171,7 @@ const runRedCheck = async ({
|
|
|
170
171
|
)
|
|
171
172
|
const sources: Record<string, string | null> = {}
|
|
172
173
|
for (const file of files) sources[file] = await readOrNull(join(path, file))
|
|
173
|
-
const
|
|
174
|
+
const checked = checkRed({
|
|
174
175
|
criteria_ids: action.criteria_ids,
|
|
175
176
|
mapping: action.mapping,
|
|
176
177
|
baseline,
|
|
@@ -178,6 +179,19 @@ const runRedCheck = async ({
|
|
|
178
179
|
test_files,
|
|
179
180
|
sources,
|
|
180
181
|
})
|
|
182
|
+
// A failed prepare after the test-writer's turn is one more problem,
|
|
183
|
+
// for the test-writer's fix loop.
|
|
184
|
+
const result =
|
|
185
|
+
prepare === null || prepare.ok
|
|
186
|
+
? checked
|
|
187
|
+
: {
|
|
188
|
+
...checked,
|
|
189
|
+
ok: false,
|
|
190
|
+
problems: [
|
|
191
|
+
`The prepare command \`${prepare.command}\` failed (exit ${prepare.exit_code ?? 'none'}):\n${prepare.output}`,
|
|
192
|
+
...checked.problems,
|
|
193
|
+
],
|
|
194
|
+
}
|
|
181
195
|
context.journal.append({
|
|
182
196
|
kind: 'red_check',
|
|
183
197
|
ticket: action.ticket,
|
|
@@ -935,6 +949,21 @@ export const executeBuildAction = async ({
|
|
|
935
949
|
}
|
|
936
950
|
case 'run_baseline_tests': {
|
|
937
951
|
const { path } = ticketWorktree({ state, ticket: action.ticket })
|
|
952
|
+
const prepare = await prepareCheck({
|
|
953
|
+
config: context.config,
|
|
954
|
+
cwd: path,
|
|
955
|
+
})
|
|
956
|
+
if (prepare !== null) {
|
|
957
|
+
journal.append({
|
|
958
|
+
kind: 'baseline_prepared',
|
|
959
|
+
ticket: action.ticket,
|
|
960
|
+
role: null,
|
|
961
|
+
content: { check: prepare },
|
|
962
|
+
})
|
|
963
|
+
// No agent has worked yet, so a failed prepare leaves the
|
|
964
|
+
// tests unrun and the ticket stuck on its output.
|
|
965
|
+
if (!prepare.ok) return
|
|
966
|
+
}
|
|
938
967
|
const run = await runBunTests({
|
|
939
968
|
cwd: path,
|
|
940
969
|
commands: bunTestCommands(context),
|
|
@@ -86,9 +86,9 @@ export const workspacePackages = async ({
|
|
|
86
86
|
* Writes the run's changeset on the run branch, commits it, and pushes the
|
|
87
87
|
* run branch, then journals `changeset_written`. It names every workspace
|
|
88
88
|
* package with files changed since the run branch's base, minus the ones
|
|
89
|
-
* the config ignores
|
|
90
|
-
*
|
|
91
|
-
* config is gone by now gets no changeset.
|
|
89
|
+
* the config ignores. A redo after a crash finds its own file already
|
|
90
|
+
* committed and commits nothing twice. A `release:none` spec (#476), or a
|
|
91
|
+
* repo whose config is gone by now, gets no changeset.
|
|
92
92
|
*/
|
|
93
93
|
export const writeChangeset = async ({
|
|
94
94
|
context,
|
|
@@ -118,21 +118,29 @@ export const writeChangeset = async ({
|
|
|
118
118
|
})
|
|
119
119
|
return
|
|
120
120
|
}
|
|
121
|
+
if (bump === 'none') {
|
|
122
|
+
await git.push({ cwd, branch })
|
|
123
|
+
journal.append({
|
|
124
|
+
kind: 'changeset_written',
|
|
125
|
+
ticket: null,
|
|
126
|
+
role: null,
|
|
127
|
+
content: {
|
|
128
|
+
path: null,
|
|
129
|
+
bump,
|
|
130
|
+
packages: [],
|
|
131
|
+
sha: await git.head({ cwd }),
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
return
|
|
135
|
+
}
|
|
121
136
|
const { ignore } = ChangesetConfigSchema.parse(
|
|
122
137
|
await readJson(join(cwd, CHANGESET_CONFIG))
|
|
123
138
|
)
|
|
124
|
-
const packages =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
files: await git.filesBetween({
|
|
130
|
-
cwd,
|
|
131
|
-
from: base_sha,
|
|
132
|
-
to: 'HEAD',
|
|
133
|
-
}),
|
|
134
|
-
ignore,
|
|
135
|
-
})
|
|
139
|
+
const packages = changedPackages({
|
|
140
|
+
packages: await workspacePackages({ cwd }),
|
|
141
|
+
files: await git.filesBetween({ cwd, from: base_sha, to: 'HEAD' }),
|
|
142
|
+
ignore,
|
|
143
|
+
})
|
|
136
144
|
const path = changesetPath({ spec_number, run_id: basename(run_dir) })
|
|
137
145
|
await Bun.write(join(cwd, path), changesetText({ packages, bump, summary }))
|
|
138
146
|
const changes = await git.changes({ cwd })
|
|
@@ -30,6 +30,8 @@ const REASON_LINES: Record<StuckReason, string> = {
|
|
|
30
30
|
join_gates_failed:
|
|
31
31
|
'The checks still fail once the ticket joins the run branch.',
|
|
32
32
|
install_failed: 'Installing the dependencies failed.',
|
|
33
|
+
prepare_failed:
|
|
34
|
+
"The repo's prepare command failed before any agent worked.",
|
|
33
35
|
setup_change_needed:
|
|
34
36
|
'An agent needs a test setup file changed, and only you may change one.',
|
|
35
37
|
crashed: 'The engine crashed in the same step, again and again.',
|
|
@@ -65,6 +67,8 @@ const suggestion = ({
|
|
|
65
67
|
return `Its join was undone, so the run branch is safe. Reply ${retry} to join again and fix it on top of the run branch, or \`skip #${n}\`.`
|
|
66
68
|
case 'install_failed':
|
|
67
69
|
return `Fix the manifest or lockfile on the base branch, then reply ${retry}.`
|
|
70
|
+
case 'prepare_failed':
|
|
71
|
+
return `Fix what the prepare command needs on this computer (a missing tool, say), or the command in \`.luca/config.json\`, then reply ${retry}.`
|
|
68
72
|
case 'setup_change_needed':
|
|
69
73
|
return `Make that change yourself in the worktree, then reply ${retry}.`
|
|
70
74
|
case 'crashed':
|
|
@@ -33,6 +33,26 @@ export const shellCheck = async ({
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Runs the config's prepare command in `cwd`, as the `prepare` check, or
|
|
38
|
+
* `null` when the config has none. The engine runs it before every test run
|
|
39
|
+
* in a checkout, so tests that need build outputs find them up to date.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const prepare = await prepareCheck({ config, cwd })
|
|
43
|
+
* if (prepare !== null && !prepare.ok) console.log(prepare.output)
|
|
44
|
+
*/
|
|
45
|
+
export const prepareCheck = async ({
|
|
46
|
+
config,
|
|
47
|
+
cwd,
|
|
48
|
+
}: {
|
|
49
|
+
config: EngineConfig
|
|
50
|
+
cwd: string
|
|
51
|
+
}): Promise<GateCheck | null> =>
|
|
52
|
+
config.prepare === undefined
|
|
53
|
+
? null
|
|
54
|
+
: shellCheck({ name: 'prepare', command: config.prepare, cwd })
|
|
55
|
+
|
|
36
56
|
/**
|
|
37
57
|
* Runs every gate the engine config names, in order: each test command,
|
|
38
58
|
* types, lint. All must pass. Unset gates are left out; intake refuses a
|
|
@@ -41,7 +61,8 @@ export const shellCheck = async ({
|
|
|
41
61
|
*
|
|
42
62
|
* With an `install` command (see `installCommand`), the engine runs it first,
|
|
43
63
|
* as the `install` check. A failed install stops there: the other gates
|
|
44
|
-
* would only fail on the missing packages.
|
|
64
|
+
* would only fail on the missing packages. The config's prepare command
|
|
65
|
+
* runs next, as the `prepare` check, and a failed one stops there too.
|
|
45
66
|
*
|
|
46
67
|
* @example
|
|
47
68
|
* const { ok, checks } = await runGates({ cwd, config, test_files, report_file, install: null })
|
|
@@ -69,6 +90,11 @@ export const runGates = async ({
|
|
|
69
90
|
checks.push(installed)
|
|
70
91
|
if (!installed.ok) return { ok: false, checks }
|
|
71
92
|
}
|
|
93
|
+
const prepared = await prepareCheck({ config, cwd })
|
|
94
|
+
if (prepared !== null) {
|
|
95
|
+
checks.push(prepared)
|
|
96
|
+
if (!prepared.ok) return { ok: false, checks }
|
|
97
|
+
}
|
|
72
98
|
const { types, lint } = config.checks
|
|
73
99
|
for (const [index, { run: command, results }] of testCommands({
|
|
74
100
|
config,
|
|
@@ -28,9 +28,16 @@ export type TestRun = z.infer<typeof TestRunSchema>
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* The gates the engine config can name, plus `install`: the engine's own
|
|
31
|
-
* install, run first when a package manifest changed
|
|
31
|
+
* install, run first when a package manifest changed, and `prepare`: the
|
|
32
|
+
* config's prepare command, run before the tests.
|
|
32
33
|
*/
|
|
33
|
-
export const GateNameSchema = z.enum([
|
|
34
|
+
export const GateNameSchema = z.enum([
|
|
35
|
+
'install',
|
|
36
|
+
'prepare',
|
|
37
|
+
'test',
|
|
38
|
+
'types',
|
|
39
|
+
'lint',
|
|
40
|
+
])
|
|
34
41
|
|
|
35
42
|
export type GateName = z.infer<typeof GateNameSchema>
|
|
36
43
|
|
|
@@ -151,6 +151,17 @@ const TicketWorktreeCreatedEntrySchema = z.object({
|
|
|
151
151
|
content: WorktreeSchema,
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The config's prepare command (#481), run in a new ticket worktree before
|
|
156
|
+
* its baseline tests. Journaled only with a prepare command. A failed one
|
|
157
|
+
* leaves the baseline tests unrun.
|
|
158
|
+
*/
|
|
159
|
+
const BaselinePreparedEntrySchema = z.object({
|
|
160
|
+
...ENTRY_FIELDS,
|
|
161
|
+
kind: z.literal('baseline_prepared'),
|
|
162
|
+
content: z.object({ check: GateCheckSchema }),
|
|
163
|
+
})
|
|
164
|
+
|
|
154
165
|
/** The tests as they stood before any agent worked on a ticket. */
|
|
155
166
|
const BaselineTestsEntrySchema = z.object({
|
|
156
167
|
...ENTRY_FIELDS,
|
|
@@ -597,6 +608,7 @@ export const StuckReasonSchema = z.enum([
|
|
|
597
608
|
'join_failed',
|
|
598
609
|
'join_gates_failed',
|
|
599
610
|
'install_failed',
|
|
611
|
+
'prepare_failed',
|
|
600
612
|
'setup_change_needed',
|
|
601
613
|
'crashed',
|
|
602
614
|
])
|
|
@@ -772,8 +784,9 @@ export type ChangesetBump = z.infer<typeof ChangesetBumpSchema>
|
|
|
772
784
|
/**
|
|
773
785
|
* The engine committed the run's one changeset (#461) at `path` on the run
|
|
774
786
|
* branch and pushed it (`sha` is the run branch's HEAD after). `packages`
|
|
775
|
-
* are the ones it names
|
|
776
|
-
* the repo no longer had a changesets
|
|
787
|
+
* are the ones it names. `path` is `null` when nothing was written: the
|
|
788
|
+
* spec is `release:none` (#476), or the repo no longer had a changesets
|
|
789
|
+
* config.
|
|
777
790
|
*/
|
|
778
791
|
const ChangesetWrittenEntrySchema = z.object({
|
|
779
792
|
...ENTRY_FIELDS,
|
|
@@ -1111,6 +1124,7 @@ export const JournalEntrySchema = z.discriminatedUnion('kind', [
|
|
|
1111
1124
|
TicketSnapshotEntrySchema,
|
|
1112
1125
|
RunBranchCreatedEntrySchema,
|
|
1113
1126
|
TicketWorktreeCreatedEntrySchema,
|
|
1127
|
+
BaselinePreparedEntrySchema,
|
|
1114
1128
|
BaselineTestsEntrySchema,
|
|
1115
1129
|
BaselineReusedEntrySchema,
|
|
1116
1130
|
AgentStartedEntrySchema,
|
|
@@ -1185,6 +1199,7 @@ export const JournalRecordSchema = z.discriminatedUnion('kind', [
|
|
|
1185
1199
|
TicketSnapshotEntrySchema.extend(STAMP_FIELDS),
|
|
1186
1200
|
RunBranchCreatedEntrySchema.extend(STAMP_FIELDS),
|
|
1187
1201
|
TicketWorktreeCreatedEntrySchema.extend(STAMP_FIELDS),
|
|
1202
|
+
BaselinePreparedEntrySchema.extend(STAMP_FIELDS),
|
|
1188
1203
|
BaselineTestsEntrySchema.extend(STAMP_FIELDS),
|
|
1189
1204
|
BaselineReusedEntrySchema.extend(STAMP_FIELDS),
|
|
1190
1205
|
AgentStartedEntrySchema.extend(STAMP_FIELDS),
|
|
@@ -1258,6 +1273,7 @@ export const JournalKindSchema = z.enum([
|
|
|
1258
1273
|
'ticket_snapshot',
|
|
1259
1274
|
'run_branch_created',
|
|
1260
1275
|
'ticket_worktree_created',
|
|
1276
|
+
'baseline_prepared',
|
|
1261
1277
|
'baseline_tests',
|
|
1262
1278
|
'baseline_reused',
|
|
1263
1279
|
'agent_started',
|
package/engine/journal/replay.ts
CHANGED
|
@@ -139,6 +139,8 @@ export type TicketProgress = {
|
|
|
139
139
|
worktree: ReplayedWorktree | null
|
|
140
140
|
/** The install in the new worktree; its `check` is `null` with no manifest. */
|
|
141
141
|
install: ReplayedInstall | null
|
|
142
|
+
/** The prepare command run before its baseline tests (`baseline_prepared`). */
|
|
143
|
+
prepare: GateCheck | null
|
|
142
144
|
/** Its own baseline test run, or the one it reused (`baseline_reused`). */
|
|
143
145
|
baseline: TestRun | null
|
|
144
146
|
/**
|
|
@@ -531,6 +533,7 @@ export type RunState = {
|
|
|
531
533
|
export const EMPTY_TICKET_PROGRESS: TicketProgress = {
|
|
532
534
|
worktree: null,
|
|
533
535
|
install: null,
|
|
536
|
+
prepare: null,
|
|
534
537
|
baseline: null,
|
|
535
538
|
baseline_sha: null,
|
|
536
539
|
test_writer: null,
|
|
@@ -1820,6 +1823,8 @@ const progressChange = ({
|
|
|
1820
1823
|
return { worktree: record.content }
|
|
1821
1824
|
case 'dependencies_installed':
|
|
1822
1825
|
return { install: { check: record.content.check } }
|
|
1826
|
+
case 'baseline_prepared':
|
|
1827
|
+
return { prepare: record.content.check }
|
|
1823
1828
|
case 'baseline_tests':
|
|
1824
1829
|
return {
|
|
1825
1830
|
baseline: record.content,
|
|
@@ -2092,8 +2097,9 @@ const retriedChange = ({
|
|
|
2092
2097
|
* branch after a rebase), then the gates;
|
|
2093
2098
|
* - in a review fix round, the round starts over as round 1 with fresh
|
|
2094
2099
|
* fixers, so the user's changes are gated, committed, and re-reviewed;
|
|
2095
|
-
* - a leftover scan, a failed install, a
|
|
2096
|
-
* failed join, or a step crashes
|
|
2100
|
+
* - a leftover scan, a failed install, a failed prepare command at the
|
|
2101
|
+
* baseline, a reviewer's failed tries, a failed join, or a step crashes
|
|
2102
|
+
* cut off just run again.
|
|
2097
2103
|
*/
|
|
2098
2104
|
const resumedProgress = ({
|
|
2099
2105
|
progress,
|
|
@@ -2117,6 +2123,7 @@ const resumedProgress = ({
|
|
|
2117
2123
|
rejoins: 0,
|
|
2118
2124
|
install:
|
|
2119
2125
|
progress.install?.check?.ok === false ? null : progress.install,
|
|
2126
|
+
prepare: progress.prepare?.ok === false ? null : progress.prepare,
|
|
2120
2127
|
leftovers: {
|
|
2121
2128
|
red: commits.red === null ? null : progress.leftovers.red,
|
|
2122
2129
|
green: commits.green === null ? null : progress.leftovers.green,
|
|
@@ -2128,6 +2135,7 @@ const resumedProgress = ({
|
|
|
2128
2135
|
if (
|
|
2129
2136
|
reason === 'leftovers_found' ||
|
|
2130
2137
|
reason === 'install_failed' ||
|
|
2138
|
+
reason === 'prepare_failed' ||
|
|
2131
2139
|
reason === 'join_failed' ||
|
|
2132
2140
|
reason === 'join_gates_failed' ||
|
|
2133
2141
|
reason === 'crashed' ||
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alecsibilia/luca",
|
|
3
3
|
"description": "Luca: turns a spec on GitHub Issues into a reviewed pull request, run from Paseo with Claude Code",
|
|
4
|
-
"version": "14.0.0-alpha.
|
|
4
|
+
"version": "14.0.0-alpha.1",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Alec Sibilia <sibilia.alec@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/asibilia/luca-framework#readme",
|