@alecsibilia/luca 14.0.0-alpha.22 → 14.0.0-alpha.23
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 +4 -0
- package/board/package.json +1 -1
- package/board/server/luca-version.ts +1 -1
- package/engine/agents/claude-options.ts +7 -3
- package/engine/core/decide-final-fix.ts +11 -4
- package/engine/core/decide-final-review.ts +3 -1
- package/engine/core/final-failure-cap.ts +8 -4
- package/engine/core/final-review-text.ts +21 -3
- package/engine/core/pull-request-text.ts +11 -7
- package/engine/core/stuck-format.ts +14 -2
- package/engine/journal/final-review-replay.ts +3 -0
- package/engine/journal/final-review-round.ts +66 -4
- package/engine/journal/final-review-state.ts +4 -1
- package/engine/journal/replay.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/board/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The Luca version of this board, stamped when the package was packed. */
|
|
2
|
-
export const LUCA_VERSION: string = "14.0.0-alpha.
|
|
2
|
+
export const LUCA_VERSION: string = "14.0.0-alpha.23"
|
|
@@ -33,15 +33,19 @@ export const MODEL_NAMES = ModelNameSchema.options
|
|
|
33
33
|
/** The model reviewers run on, and every agent the decision model doesn't route (#603). */
|
|
34
34
|
export const CLAUDE_MODEL = DEFAULT_MODEL
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* How many model turns one agent gets before its try ends. The integration
|
|
38
|
+
* lens reads how every ticket fits together on the whole branch, so it gets
|
|
39
|
+
* more than the other lenses.
|
|
40
|
+
*/
|
|
37
41
|
export const MAX_TURNS: Record<AgentRole, number> = {
|
|
38
42
|
'test-writer': 80,
|
|
39
|
-
implementer:
|
|
43
|
+
implementer: 150,
|
|
40
44
|
'ticket-reviewer': 60,
|
|
41
45
|
'architecture-lens': 60,
|
|
42
46
|
'simplification-lens': 60,
|
|
43
47
|
'security-lens': 60,
|
|
44
|
-
'integration-lens':
|
|
48
|
+
'integration-lens': 100,
|
|
45
49
|
'rules-lens': 60,
|
|
46
50
|
learner: 40,
|
|
47
51
|
}
|
|
@@ -29,10 +29,11 @@ import type { AgentRole } from '../agents/role-results'
|
|
|
29
29
|
import { handoffRole, type ReplayedAgentFailure } from '../journal/replay'
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* A fresh fixer of this role, told its findings (or the failed gates).
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* A fresh fixer of this role, told its findings (or the failed gates). The
|
|
33
|
+
* implementer that starts a round whose slow tests failed is told of them
|
|
34
|
+
* too (#563). With `follow_up`, it stands in for a follow-up whose session
|
|
35
|
+
* is gone, and its prompt carries the message. A fixer taking a turn a
|
|
36
|
+
* crash cut off is told of the crash.
|
|
36
37
|
*/
|
|
37
38
|
const launchFixer = ({
|
|
38
39
|
role,
|
|
@@ -49,6 +50,11 @@ const launchFixer = ({
|
|
|
49
50
|
// An implementer with no code findings left is here for failed gates.
|
|
50
51
|
const gates =
|
|
51
52
|
role === 'implementer' && fix.code_answered ? review.gates : null
|
|
53
|
+
// The implementer with code findings also fixes the round's slow tests.
|
|
54
|
+
const slow_tests =
|
|
55
|
+
role === 'implementer' && !fix.code_answered && review.slow_due
|
|
56
|
+
? review.slow_tests
|
|
57
|
+
: null
|
|
52
58
|
const sections = [
|
|
53
59
|
...(follow_up === undefined
|
|
54
60
|
? []
|
|
@@ -63,6 +69,7 @@ const launchFixer = ({
|
|
|
63
69
|
snapshot,
|
|
64
70
|
fix,
|
|
65
71
|
gates,
|
|
72
|
+
slow_tests,
|
|
66
73
|
run_notes,
|
|
67
74
|
sections,
|
|
68
75
|
}),
|
|
@@ -69,7 +69,9 @@ const lensStep = ({
|
|
|
69
69
|
* earlier findings; its blocking findings get one fix round (past the round
|
|
70
70
|
* cap, too) and one re-review, and what is still open then tops the PR
|
|
71
71
|
* instead of getting the review stuck.
|
|
72
|
-
* Failed tries are capped per role
|
|
72
|
+
* Failed tries are capped per role, in a row: a finished turn of the role
|
|
73
|
+
* gives its tries back, so one failed try in an early round doesn't count
|
|
74
|
+
* against it in a later one. A stuck final review
|
|
73
75
|
* waits for a reply; `ship` opens the PR anyway. A final review step cut
|
|
74
76
|
* off by a crash `MAX_CRASHES` times in a row is stuck too.
|
|
75
77
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The cap on a final review role's failed tries (`failureCap`): the stuck step
|
|
3
|
-
* once a failed turn used up the role's tries, or its engine
|
|
3
|
+
* once a failed turn used up the role's tries in a row, or its engine
|
|
4
|
+
* failures in a row. A finished turn of the role gives its tries back.
|
|
4
5
|
*/
|
|
5
6
|
import { stuck, type FinalReviewAction } from './final-review-action'
|
|
6
7
|
import { MAX_ENGINE_FAILURES, MAX_FIX_ROUNDS } from './loop-caps'
|
|
@@ -8,8 +9,11 @@ import { MAX_ENGINE_FAILURES, MAX_FIX_ROUNDS } from './loop-caps'
|
|
|
8
9
|
import type { FinalReviewState, ReplayedAgentFailure } from '../journal/replay'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* The stuck step when a failed turn used up its role's tries (or
|
|
12
|
-
* failures in a row), else `null`: the role gets another go.
|
|
12
|
+
* The stuck step when a failed turn used up its role's tries in a row (or
|
|
13
|
+
* engine failures in a row), else `null`: the role gets another go.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* failureCap({ review, failed }) // null while the role has tries left
|
|
13
17
|
*/
|
|
14
18
|
export const failureCap = ({
|
|
15
19
|
review,
|
|
@@ -32,7 +36,7 @@ export const failureCap = ({
|
|
|
32
36
|
return tries >= MAX_FIX_ROUNDS
|
|
33
37
|
? stuck({
|
|
34
38
|
reason: 'agent_failed',
|
|
35
|
-
detail: `The ${role} failed ${tries} tries in the final review; the last one: ${error}`,
|
|
39
|
+
detail: `The ${role} failed ${tries} tries in a row in the final review; the last one: ${error}`,
|
|
36
40
|
})
|
|
37
41
|
: null
|
|
38
42
|
}
|
|
@@ -190,13 +190,27 @@ const findingsSection = ({
|
|
|
190
190
|
FIX_RULES,
|
|
191
191
|
].join('\n\n')
|
|
192
192
|
|
|
193
|
+
/** The round's failed slow tests, beside the implementer's code findings (#563). */
|
|
194
|
+
const slowTestsSection = ({
|
|
195
|
+
slow_tests,
|
|
196
|
+
}: {
|
|
197
|
+
slow_tests: ReplayedGates
|
|
198
|
+
}): string =>
|
|
199
|
+
[
|
|
200
|
+
'## The slow tests failed too',
|
|
201
|
+
'Once every lens finished, the engine ran the slow tests on the run branch, and these failed. Fix the code so they pass too: the gates run them again after you answer.',
|
|
202
|
+
failedChecks({ gates: slow_tests }),
|
|
203
|
+
].join('\n\n')
|
|
204
|
+
|
|
193
205
|
/**
|
|
194
206
|
* The prompt a fresh final review fixer starts with: its task, the spec,
|
|
195
207
|
* every ticket, and its findings (the test findings for the test-writer,
|
|
196
208
|
* the code findings for the implementer, or the findings the other fixer
|
|
197
|
-
* answered `other_role`, #616).
|
|
198
|
-
*
|
|
199
|
-
*
|
|
209
|
+
* answered `other_role`, #616). The implementer that starts a round whose
|
|
210
|
+
* slow tests failed (#563) gets their output beside its code findings. An
|
|
211
|
+
* implementer launched only to fix failed gates (the round had only test
|
|
212
|
+
* findings) gets the gates' output instead. The engine journals it word
|
|
213
|
+
* for word.
|
|
200
214
|
*
|
|
201
215
|
* @example
|
|
202
216
|
* const prompt = finalFixerPrompt({ role: 'implementer', snapshot, fix, gates: null })
|
|
@@ -206,6 +220,7 @@ export const finalFixerPrompt = ({
|
|
|
206
220
|
snapshot,
|
|
207
221
|
fix,
|
|
208
222
|
gates,
|
|
223
|
+
slow_tests,
|
|
209
224
|
run_notes,
|
|
210
225
|
sections,
|
|
211
226
|
}: {
|
|
@@ -214,6 +229,8 @@ export const finalFixerPrompt = ({
|
|
|
214
229
|
fix: FinalReviewFix
|
|
215
230
|
/** Failed gates to fix, for an implementer with no findings left. */
|
|
216
231
|
gates: ReplayedGates | null
|
|
232
|
+
/** The round's failed slow tests, for the implementer that starts it. */
|
|
233
|
+
slow_tests?: ReplayedGates | null
|
|
217
234
|
/** Notes earlier agents in the run left, oldest first. */
|
|
218
235
|
run_notes?: PromptRunNote[]
|
|
219
236
|
/** More sections after the task, such as a follow-up's message. */
|
|
@@ -239,6 +256,7 @@ export const finalFixerPrompt = ({
|
|
|
239
256
|
FIXER_TASKS[role],
|
|
240
257
|
...workSections({ snapshot }),
|
|
241
258
|
task,
|
|
259
|
+
...(slow_tests?.ok === false ? [slowTestsSection({ slow_tests })] : []),
|
|
242
260
|
...(sections ?? []),
|
|
243
261
|
...runNotesSection({ run_notes: run_notes ?? [] }),
|
|
244
262
|
].join('\n\n')
|
|
@@ -15,6 +15,7 @@ import { reasonLine } from './stuck-text'
|
|
|
15
15
|
import type { AgentRole, MergeDanger } from '../agents/role-results'
|
|
16
16
|
import {
|
|
17
17
|
EMPTY_FINAL_REVIEW,
|
|
18
|
+
finalPullRequestFindings,
|
|
18
19
|
type FinalReviewState,
|
|
19
20
|
type ReplayedSnapshot,
|
|
20
21
|
type TicketProgress,
|
|
@@ -114,7 +115,9 @@ const outOfTurnsSection = ({
|
|
|
114
115
|
* agents ran out of turns (#543), the reviews' nits,
|
|
115
116
|
* the findings declined through "won't fix", and the memories the learner
|
|
116
117
|
* added or updated (#370). A final review shipped
|
|
117
|
-
* while stuck puts its open findings at the very top
|
|
118
|
+
* while stuck puts its open findings at the very top; a round it never
|
|
119
|
+
* settled counts the findings, nits, and rulings of the lenses that
|
|
120
|
+
* finished it (see `finalPullRequestFindings`). Right after the
|
|
118
121
|
* "Built by" line come the Summary picture and the Merge Danger (from the
|
|
119
122
|
* final review's integration lens, or "Not available" without them) and
|
|
120
123
|
* the Evidence that each ticket's new tests failed first and pass now
|
|
@@ -137,6 +140,8 @@ export const pullRequestText = ({
|
|
|
137
140
|
memory_saves?: MemorySave[]
|
|
138
141
|
}): { title: string; body: string } => {
|
|
139
142
|
const review = final_review ?? EMPTY_FINAL_REVIEW
|
|
143
|
+
// A round a stuck lens never settled keeps its finished lenses' findings.
|
|
144
|
+
const findings = finalPullRequestFindings({ review })
|
|
140
145
|
const { spec } = snapshot
|
|
141
146
|
const title = (number: number) => snapshot.tickets[number]?.title ?? ''
|
|
142
147
|
const isSkipped = (number: number) =>
|
|
@@ -175,7 +180,7 @@ export const pullRequestText = ({
|
|
|
175
180
|
`- #${number} ${id}${fileText(file)}: ${title}`
|
|
176
181
|
)
|
|
177
182
|
),
|
|
178
|
-
...
|
|
183
|
+
...findings.nits.map(
|
|
179
184
|
({ lens, id, title, file }) =>
|
|
180
185
|
`- final review, ${lens} lens: ${id}${fileText(file)}: ${title}`
|
|
181
186
|
),
|
|
@@ -206,23 +211,22 @@ export const pullRequestText = ({
|
|
|
206
211
|
declinedLine({ where: `#${number} `, ...entry })
|
|
207
212
|
)
|
|
208
213
|
),
|
|
209
|
-
...
|
|
214
|
+
...findings.declined.map((entry) =>
|
|
210
215
|
declinedLine({
|
|
211
216
|
where: `final review, ${entry.lens} lens: `,
|
|
212
217
|
...entry,
|
|
213
218
|
})
|
|
214
219
|
),
|
|
215
220
|
]
|
|
216
|
-
const stillOpen = review.fix?.findings ?? []
|
|
217
221
|
const shipped =
|
|
218
222
|
review.shipped && review.stuck !== null
|
|
219
223
|
? shippedFindingsSection({
|
|
220
|
-
findings:
|
|
224
|
+
findings: findings.open,
|
|
221
225
|
stuck: review.stuck,
|
|
222
226
|
edits: review.shipped_edits,
|
|
223
227
|
})
|
|
224
|
-
: review.fresh_started &&
|
|
225
|
-
? freshFindingsSection({ findings:
|
|
228
|
+
: review.fresh_started && findings.open.length > 0
|
|
229
|
+
? freshFindingsSection({ findings: findings.open })
|
|
226
230
|
: ''
|
|
227
231
|
const body = [
|
|
228
232
|
shipped,
|
|
@@ -18,5 +18,17 @@ export const fenced = (text: string): string => {
|
|
|
18
18
|
return `${fence}\n${clipped}\n${fence}`
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* `count` and `word`, with `s` past one; a word ending in a consonant and
|
|
23
|
+
* `y` ends in `ies` instead.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* plural(2, 'fix round') // '2 fix rounds'
|
|
27
|
+
* plural(3, 'failed try') // '3 failed tries'
|
|
28
|
+
*/
|
|
29
|
+
export const plural = (count: number, word: string): string => {
|
|
30
|
+
if (count === 1) return `${count} ${word}`
|
|
31
|
+
return /[^aeiou]y$/.test(word)
|
|
32
|
+
? `${count} ${word.slice(0, -1)}ies`
|
|
33
|
+
: `${count} ${word}s`
|
|
34
|
+
}
|
|
@@ -143,7 +143,10 @@ export const finalReviewAfter = ({
|
|
|
143
143
|
return finalResultChange({
|
|
144
144
|
review: {
|
|
145
145
|
...review,
|
|
146
|
+
// A finished turn gives the role its tries back: the
|
|
147
|
+
// cap is on failed tries in a row.
|
|
146
148
|
agent_failures: omit(review.agent_failures, role),
|
|
149
|
+
failed_tries: omit(review.failed_tries, role),
|
|
147
150
|
engine_failures: omit(review.engine_failures, role),
|
|
148
151
|
sessions: sessionsAfter({
|
|
149
152
|
sessions: review.sessions,
|
|
@@ -13,7 +13,11 @@ import {
|
|
|
13
13
|
handoffRole,
|
|
14
14
|
type FinishedContent,
|
|
15
15
|
} from './agent-turn-replay'
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
FinalDeclined,
|
|
18
|
+
FinalFinding,
|
|
19
|
+
FinalReviewState,
|
|
20
|
+
} from './final-review-state'
|
|
17
21
|
import { setupChangeOf } from './ticket-result-replay'
|
|
18
22
|
|
|
19
23
|
import {
|
|
@@ -112,6 +116,21 @@ const lensResultOf = ({
|
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The blocking findings of each due lens that finished this round, with
|
|
121
|
+
* its lens.
|
|
122
|
+
*/
|
|
123
|
+
const roundBlocking = ({
|
|
124
|
+
review,
|
|
125
|
+
}: {
|
|
126
|
+
review: FinalReviewState
|
|
127
|
+
}): FinalFinding[] =>
|
|
128
|
+
review.lenses_due.flatMap((lens) =>
|
|
129
|
+
(review.results[lens]?.findings ?? [])
|
|
130
|
+
.filter(isBlocking)
|
|
131
|
+
.map((entry) => ({ ...entry, lens }))
|
|
132
|
+
)
|
|
133
|
+
|
|
115
134
|
/**
|
|
116
135
|
* Every due lens finished this round, and its slow tests ran if they were
|
|
117
136
|
* due (#563): the round can settle.
|
|
@@ -161,9 +180,7 @@ export const settleRound = ({
|
|
|
161
180
|
result.findings.filter((entry) => !isBlocking(entry))
|
|
162
181
|
)
|
|
163
182
|
)
|
|
164
|
-
const blocking =
|
|
165
|
-
tagged(lens, result.findings.filter(isBlocking))
|
|
166
|
-
)
|
|
183
|
+
const blocking = roundBlocking({ review })
|
|
167
184
|
const settled: FinalReviewState = {
|
|
168
185
|
...review,
|
|
169
186
|
declined: [...review.declined, ...declined],
|
|
@@ -199,6 +216,51 @@ export const settleRound = ({
|
|
|
199
216
|
}
|
|
200
217
|
}
|
|
201
218
|
|
|
219
|
+
/** The final review's findings as its PR lists them. */
|
|
220
|
+
export type PullRequestFindings = {
|
|
221
|
+
/** The blocking findings still open. */
|
|
222
|
+
open: FinalFinding[]
|
|
223
|
+
nits: FinalFinding[]
|
|
224
|
+
declined: FinalDeclined[]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The final review's findings as its PR lists them: the blocking ones still
|
|
229
|
+
* open, the nits, and the "won't fix" answers a lens let go. A round that
|
|
230
|
+
* never settled (a lens failed on every try, or a step crashed, and the
|
|
231
|
+
* owner replied `ship`) is settled over the lenses that finished it, as
|
|
232
|
+
* `settleRound` would: their blocking findings, nits, and rulings count,
|
|
233
|
+
* and a finding one of them cleared is no longer open. The open fix round's
|
|
234
|
+
* findings stay open only for the lenses that never finished. A settled
|
|
235
|
+
* round's findings are as they stand.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* finalPullRequestFindings({ review }).open
|
|
239
|
+
* // [{ id: 'simplification-F1', lens: 'simplification', severity: 'should_fix', ... }]
|
|
240
|
+
*/
|
|
241
|
+
export const finalPullRequestFindings = ({
|
|
242
|
+
review,
|
|
243
|
+
}: {
|
|
244
|
+
review: FinalReviewState
|
|
245
|
+
}): PullRequestFindings => {
|
|
246
|
+
const earlier = review.fix?.findings ?? []
|
|
247
|
+
const finished = review.lenses_due.filter(
|
|
248
|
+
(lens) => review.results[lens] !== undefined
|
|
249
|
+
)
|
|
250
|
+
if (roundDone({ review }) || finished.length === 0) {
|
|
251
|
+
return { open: earlier, nits: review.nits, declined: review.declined }
|
|
252
|
+
}
|
|
253
|
+
const settled = settleRound({ review: { ...review, lenses_due: finished } })
|
|
254
|
+
return {
|
|
255
|
+
open: [
|
|
256
|
+
...earlier.filter(({ lens }) => !finished.includes(lens)),
|
|
257
|
+
...(settled.fix?.findings ?? []),
|
|
258
|
+
],
|
|
259
|
+
nits: settled.nits,
|
|
260
|
+
declined: settled.declined,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
202
264
|
/**
|
|
203
265
|
* Where a finished agent's result goes in the final review: a lens's
|
|
204
266
|
* result this round (settling the round once every due lens finished), or
|
|
@@ -72,7 +72,10 @@ export type FinalReviewState = {
|
|
|
72
72
|
sessions: Partial<Record<AgentRole, string>>
|
|
73
73
|
/** Each role's failed turn, if no agent of that role finished after it. */
|
|
74
74
|
agent_failures: Partial<Record<AgentRole, ReplayedAgentFailure>>
|
|
75
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Failed tries (agent, result, guard) per role, in a row: a finished
|
|
77
|
+
* turn of the role starts its count again.
|
|
78
|
+
*/
|
|
76
79
|
failed_tries: Partial<Record<AgentRole, number>>
|
|
77
80
|
/** Engine failures in a row, per role. */
|
|
78
81
|
engine_failures: Partial<Record<AgentRole, number>>
|
package/engine/journal/replay.ts
CHANGED
|
@@ -71,7 +71,7 @@ export { EMPTY_FINAL_REVIEW } from './final-review-state'
|
|
|
71
71
|
export type { PlanState } from './plan-replay'
|
|
72
72
|
export type { LearnerState, MemoryState } from './memory-replay'
|
|
73
73
|
export { EMPTY_MEMORY } from './memory-replay'
|
|
74
|
-
export { namespacedId } from './final-review-round'
|
|
74
|
+
export { finalPullRequestFindings, namespacedId } from './final-review-round'
|
|
75
75
|
export { isFinalKey } from './final-review-replay'
|
|
76
76
|
export { handoffRole } from './agent-turn-replay'
|
|
77
77
|
|
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.23",
|
|
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",
|