@alecsibilia/luca 14.0.0-alpha.0 → 14.0.0-alpha.2
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 +10 -0
- package/README.md +5 -7
- package/board/index.server.ts +4 -29
- package/board/package.json +1 -1
- package/board/server/board-vocabulary.ts +12 -1
- package/board/server/luca-version.ts +2 -0
- package/board/server/make-rows.ts +24 -2
- package/board/server/reduce-board.ts +42 -1
- package/board/shared/board-rpc.ts +2 -2
- package/engine/agents/role-instructions.ts +3 -1
- package/engine/agents/role-prompts.ts +17 -0
- package/engine/agents/role-results.ts +39 -5
- 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 +31 -0
- package/engine/core/already-done.ts +139 -0
- package/engine/core/changeset.ts +6 -7
- package/engine/core/decide-build.ts +133 -18
- package/engine/core/decide-usage.ts +5 -3
- package/engine/core/decide.ts +10 -4
- package/engine/core/execute-already-done.ts +65 -0
- package/engine/core/execute-build.ts +138 -24
- package/engine/core/execute-changeset.ts +28 -16
- package/engine/core/execute.ts +13 -1
- package/engine/core/learner-digest.ts +3 -1
- package/engine/core/not-in-run.ts +29 -0
- package/engine/core/pull-request-text.ts +45 -4
- package/engine/core/session-close.ts +10 -3
- package/engine/core/stuck-text.ts +7 -2
- package/engine/gates/gate-runner.ts +77 -6
- package/engine/gates/gate-schemas.ts +23 -2
- package/engine/gates/leftover-scan.ts +7 -2
- package/engine/git/git-adapter.ts +69 -4
- package/engine/git/path-text.ts +34 -0
- package/engine/guards/worktree-state.ts +14 -2
- package/engine/journal/journal-record.ts +80 -3
- package/engine/journal/replay.ts +51 -2
- package/engine/shell/run-command.ts +109 -7
- package/engine/testing/build-fixtures.ts +66 -0
- package/engine/tracker/github-tracker.ts +5 -0
- package/engine/tracker/in-memory-tracker.ts +3 -0
- package/engine/tracker/tracker.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @alecsibilia/luca
|
|
2
2
|
|
|
3
|
+
## 14.0.0-alpha.2
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 14.0.0-alpha.1
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 8a58510: Spec: Luca v14 fixes: no empty changesets, board version, init sets up the repo ([#480](https://github.com/asibilia/luca-framework/issues/480))
|
|
12
|
+
|
|
3
13
|
## 14.0.0-alpha.0
|
|
4
14
|
|
|
5
15
|
### 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
|
@@ -128,7 +128,10 @@ export const BOARD_VOCABULARY = {
|
|
|
128
128
|
})
|
|
129
129
|
),
|
|
130
130
|
}),
|
|
131
|
-
|
|
131
|
+
/** The run ends with nothing to do; `already_done` is empty at intake (#484). */
|
|
132
|
+
nothing_to_do: z.looseObject({
|
|
133
|
+
already_done: z.array(z.number().int()).catch([]).default([]),
|
|
134
|
+
}),
|
|
132
135
|
spec_snapshot: z.looseObject({
|
|
133
136
|
spec: z.looseObject({ number: z.number(), title: z.string() }),
|
|
134
137
|
ticket_order: z.array(z.number()).catch([]),
|
|
@@ -313,6 +316,12 @@ export const BOARD_VOCABULARY = {
|
|
|
313
316
|
ticket_skipped: z.looseObject({
|
|
314
317
|
because: z.number().int().nullable().catch(null),
|
|
315
318
|
}),
|
|
319
|
+
/** A ticket whose work was already on the base branch (#484), done by `shas`. */
|
|
320
|
+
ticket_already_done: z.looseObject({
|
|
321
|
+
shas: z.array(z.string()).catch([]),
|
|
322
|
+
}),
|
|
323
|
+
/** The engine closed an already-done ticket, as the run opens no PR. */
|
|
324
|
+
ticket_closed: z.looseObject({}),
|
|
316
325
|
|
|
317
326
|
// The final review (#367). Its agent, gate, scan, commit, and push
|
|
318
327
|
// records are the usual kinds with `ticket: null`.
|
|
@@ -428,6 +437,8 @@ const BoardEntrySchema = z.discriminatedUnion('kind', [
|
|
|
428
437
|
entry({ kind: 'reply_ignored' }),
|
|
429
438
|
entry({ kind: 'ticket_retried' }),
|
|
430
439
|
entry({ kind: 'ticket_skipped' }),
|
|
440
|
+
entry({ kind: 'ticket_already_done' }),
|
|
441
|
+
entry({ kind: 'ticket_closed' }),
|
|
431
442
|
entry({ kind: 'final_review_started' }),
|
|
432
443
|
entry({ kind: 'lens_started' }),
|
|
433
444
|
entry({ kind: 'lens_finished' }),
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
runReasonText,
|
|
12
12
|
reviewCountsText,
|
|
13
13
|
roleWords,
|
|
14
|
+
shortSha,
|
|
14
15
|
startKind,
|
|
15
16
|
} from './reduce-board'
|
|
16
17
|
|
|
@@ -216,11 +217,16 @@ export const describeRecord = ({
|
|
|
216
217
|
text: `Intake refused the run. ${after.run.refusal.join('; ')}`,
|
|
217
218
|
tone: 'danger',
|
|
218
219
|
})
|
|
219
|
-
case 'nothing_to_do':
|
|
220
|
+
case 'nothing_to_do': {
|
|
221
|
+
const done = record.content.already_done
|
|
220
222
|
return event({
|
|
221
|
-
text:
|
|
223
|
+
text:
|
|
224
|
+
done.length === 0
|
|
225
|
+
? 'Nothing to do: the spec has no open tickets.'
|
|
226
|
+
: `Nothing to do: the work of ${done.map((number) => `#${number}`).join(', ')} was already on the base branch.`,
|
|
222
227
|
tone: 'info',
|
|
223
228
|
})
|
|
229
|
+
}
|
|
224
230
|
case 'spec_snapshot':
|
|
225
231
|
return event({
|
|
226
232
|
text: `Intake passed: spec #${record.content.spec.number}, ${record.content.ticket_order.length} tickets.`,
|
|
@@ -389,6 +395,16 @@ export const describeRecord = ({
|
|
|
389
395
|
tone: 'info',
|
|
390
396
|
})
|
|
391
397
|
}
|
|
398
|
+
case 'ticket_already_done':
|
|
399
|
+
return event({
|
|
400
|
+
text: `${at}already done on the base branch, by ${record.content.shas.map((sha) => shortSha({ sha })).join(', ')}. Nothing to build.`,
|
|
401
|
+
tone: 'success',
|
|
402
|
+
})
|
|
403
|
+
case 'ticket_closed':
|
|
404
|
+
return event({
|
|
405
|
+
text: `${at}closed, since its work was already done.`,
|
|
406
|
+
tone: 'info',
|
|
407
|
+
})
|
|
392
408
|
case 'ticket_skipped': {
|
|
393
409
|
const { because } = record.content
|
|
394
410
|
return event({
|
|
@@ -689,6 +705,12 @@ const finishedText = ({
|
|
|
689
705
|
})
|
|
690
706
|
}
|
|
691
707
|
if (role === 'test-writer') {
|
|
708
|
+
if (result.outcome === 'already_done') {
|
|
709
|
+
return event({
|
|
710
|
+
text: `${at}the test-writer found the work already done.`,
|
|
711
|
+
tone: 'info',
|
|
712
|
+
})
|
|
713
|
+
}
|
|
692
714
|
return result.outcome === 'nothing_new_to_test'
|
|
693
715
|
? event({ text: `${at}nothing new to test.`, tone: 'info' })
|
|
694
716
|
: event({ text: `${at}tests written.`, tone: 'info' })
|
|
@@ -40,7 +40,7 @@ const STUCK_REASONS: Record<string, string> = {
|
|
|
40
40
|
agent_failed: 'An agent failed on its last try.',
|
|
41
41
|
red_check_failed: 'The red check failed.',
|
|
42
42
|
nothing_new_to_test:
|
|
43
|
-
'The test-writer
|
|
43
|
+
'The test-writer says the ticket changes no behavior, so there is nothing new to test. Label it refactor and retry it.',
|
|
44
44
|
leftovers_found:
|
|
45
45
|
'The leftover scan found files that must not be committed.',
|
|
46
46
|
gates_failed: 'The checks failed.',
|
|
@@ -49,11 +49,30 @@ 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.',
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A commit's short sha, as git and GitHub show it.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* shortSha({ sha: '3559c25f5a1b' }) // '3559c25'
|
|
63
|
+
*/
|
|
64
|
+
export const shortSha = ({ sha }: { sha: string }): string => sha.slice(0, 7)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An already-done ticket's activity (#484): calm, with the commits that
|
|
68
|
+
* did its work.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* alreadyDoneText({ shas: ['3559c25f5a1b'] }) // 'already done · 3559c25'
|
|
72
|
+
*/
|
|
73
|
+
export const alreadyDoneText = ({ shas }: { shas: string[] }): string =>
|
|
74
|
+
['already done', ...shas.map((sha) => shortSha({ sha }))].join(' · ')
|
|
75
|
+
|
|
57
76
|
/** Why a retry of an edited ticket was refused, in words. */
|
|
58
77
|
export const RETRY_REFUSED_TEXT =
|
|
59
78
|
"Its new text or labels aren't ready to build."
|
|
@@ -375,6 +394,8 @@ const STEP_WORDS: Record<string, string> = {
|
|
|
375
394
|
push_final_fixes: 'pushing the run branch',
|
|
376
395
|
write_changeset: 'writing the changeset',
|
|
377
396
|
open_pull_request: 'opening the pull request',
|
|
397
|
+
mark_already_done: 'noting the work is already done',
|
|
398
|
+
close_ticket: 'closing the ticket',
|
|
378
399
|
}
|
|
379
400
|
|
|
380
401
|
/**
|
|
@@ -1274,6 +1295,23 @@ const applyKind = ({
|
|
|
1274
1295
|
ticket,
|
|
1275
1296
|
because: record.content.because,
|
|
1276
1297
|
})
|
|
1298
|
+
// Done, not stuck (#484): its work was already on the base branch.
|
|
1299
|
+
case 'ticket_already_done':
|
|
1300
|
+
return updateTicket({
|
|
1301
|
+
state,
|
|
1302
|
+
number: ticket,
|
|
1303
|
+
update: (card) => ({
|
|
1304
|
+
...card,
|
|
1305
|
+
stage: 'done',
|
|
1306
|
+
step: ALL_STEPS_DONE,
|
|
1307
|
+
role: null,
|
|
1308
|
+
open_check: null,
|
|
1309
|
+
activity: alreadyDoneText({ shas: record.content.shas }),
|
|
1310
|
+
}),
|
|
1311
|
+
})
|
|
1312
|
+
// Its closing changes nothing the board shows.
|
|
1313
|
+
case 'ticket_closed':
|
|
1314
|
+
return state
|
|
1277
1315
|
case 'ticket_retried':
|
|
1278
1316
|
return ticketRetried({ state, record })
|
|
1279
1317
|
// An ignored reply is only a chat row; the engine answered it.
|
|
@@ -1576,6 +1614,9 @@ const agentFinished = ({
|
|
|
1576
1614
|
if (card.open_check === 'review') {
|
|
1577
1615
|
return { ...done, activity: 'test findings answered' }
|
|
1578
1616
|
}
|
|
1617
|
+
if (result.outcome === 'already_done') {
|
|
1618
|
+
return { ...done, activity: 'already done' }
|
|
1619
|
+
}
|
|
1579
1620
|
return result.outcome === 'nothing_new_to_test'
|
|
1580
1621
|
? { ...done, step: 2, activity: 'nothing new to test' }
|
|
1581
1622
|
: { ...done, step: 1, activity: 'red check' }
|
|
@@ -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(),
|
|
@@ -82,8 +82,10 @@ You write the failing tests for ONE ticket, before any code exists.
|
|
|
82
82
|
- Give every test a plain string-literal name (no test.each, no template names), so the engine can find each one before the module exists.
|
|
83
83
|
|
|
84
84
|
Your result (structured output):
|
|
85
|
-
- outcome: "tests_written", or "nothing_new_to_test" if the ticket truly changes no behavior (a refactor), or "needs_setup_change" if the tests can't work without a change to a test setup file${setupFiles({ config })} (you may never change one): say which file and why in "setup_change", and the user makes the change.
|
|
85
|
+
- outcome: "tests_written", or "nothing_new_to_test" if the ticket truly changes no behavior (a refactor), or "already_done" if the ticket's work is already on the base branch, or "needs_setup_change" if the tests can't work without a change to a test setup file${setupFiles({ config })} (you may never change one): say which file and why in "setup_change", and the user makes the change.
|
|
86
|
+
- Answer "already_done" only when EVERY acceptance criterion is already met on the base branch and already has a test that checks it. Look with \`git log\` and \`git show\`, and read the code and tests. Then write no tests, and give your evidence: in "done_by", the commits that did the work, as { sha, title }; in "criteria", the existing tests that cover each criterion. If even one criterion is not met, or not tested, write the tests instead.
|
|
86
87
|
- criteria: for EACH criterion id (AC1, AC2, ...), the tests that check it, as { file, name }. "name" is the full name bun prints: describe names and the test name joined by " > ".
|
|
88
|
+
- done_by: empty, unless the outcome is "already_done".
|
|
87
89
|
- finding_responses: empty, unless your prompt gives you ticket review findings (see below).
|
|
88
90
|
- summary, assumptions, run_notes.
|
|
89
91
|
|
|
@@ -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.'
|
|
@@ -126,21 +126,39 @@ const SHARED_FIELDS = {
|
|
|
126
126
|
run_notes: z.array(z.string()).default([]),
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** A commit on the base branch that did a ticket's work (#484). */
|
|
130
|
+
export const DoneCommitSchema = z.object({
|
|
131
|
+
sha: z.string().min(1),
|
|
132
|
+
/** Its subject line, if the agent read it. */
|
|
133
|
+
title: z.string().default(''),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
export type DoneCommit = z.infer<typeof DoneCommitSchema>
|
|
137
|
+
|
|
129
138
|
/**
|
|
130
139
|
* The test-writer's result. `nothing_new_to_test` is an honest answer for a
|
|
131
140
|
* ticket that changes no behavior; the engine then makes the ticket stuck,
|
|
132
|
-
* with a hint to label it `refactor`. `
|
|
133
|
-
*
|
|
134
|
-
*
|
|
141
|
+
* with a hint to label it `refactor`. `already_done` says the ticket's work
|
|
142
|
+
* is already on the base branch (#484): `done_by` names the commits that did
|
|
143
|
+
* it and `criteria` the existing tests that cover each criterion; the engine
|
|
144
|
+
* counts the ticket as done. `needs_setup_change` says the tests can't be
|
|
145
|
+
* written without a change to a test setup file (`setup_change`), which
|
|
146
|
+
* only the user may make.
|
|
135
147
|
*/
|
|
136
148
|
export const TestWriterResultSchema = z.object({
|
|
137
149
|
outcome: z.enum([
|
|
138
150
|
'tests_written',
|
|
139
151
|
'nothing_new_to_test',
|
|
152
|
+
'already_done',
|
|
140
153
|
'needs_setup_change',
|
|
141
154
|
]),
|
|
142
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* For each criterion id (AC1, AC2, ...), the tests that check it: new
|
|
157
|
+
* ones, or with `already_done`, the ones already on the base branch.
|
|
158
|
+
*/
|
|
143
159
|
criteria: z.array(CriterionTestsSchema).default([]),
|
|
160
|
+
/** With `already_done`: the base branch's commits that did the work. */
|
|
161
|
+
done_by: z.array(DoneCommitSchema).default([]),
|
|
144
162
|
...SETUP_FIELDS,
|
|
145
163
|
...SHARED_FIELDS,
|
|
146
164
|
...FIXER_FIELDS,
|
|
@@ -317,8 +335,24 @@ export const parseRoleResult = ({
|
|
|
317
335
|
error: `The ${role}'s result does not fit its schema:\n${z.prettifyError(parsed.error)}`,
|
|
318
336
|
}
|
|
319
337
|
}
|
|
320
|
-
// Every reviewer (the ticket reviewer and each lens) gives a verdict.
|
|
321
338
|
const { result } = parsed.data
|
|
339
|
+
// "Already done" is only an answer with its evidence (#484).
|
|
340
|
+
if (parsed.data.role === 'test-writer') {
|
|
341
|
+
const { outcome, done_by, criteria } = parsed.data.result
|
|
342
|
+
if (outcome === 'already_done' && done_by.length === 0) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
error: 'The test-writer\'s "already_done" names no commit: list in "done_by" the base branch\'s commits that did the work.',
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (outcome === 'already_done' && criteria.length === 0) {
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
error: 'The test-writer\'s "already_done" names no tests: list in "criteria" the existing tests that cover each criterion.',
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Every reviewer (the ticket reviewer and each lens) gives a verdict.
|
|
322
356
|
if ('verdict' in result) {
|
|
323
357
|
const { verdict, findings } = result
|
|
324
358
|
const expected = findings.some(isBlocking)
|
|
@@ -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`
|