@alecsibilia/luca 14.0.0-alpha.21 → 14.0.0-alpha.22
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/agent-launcher.ts +7 -2
- package/engine/agents/claude-launcher-helpers.ts +6 -1
- package/engine/agents/claude-launcher.ts +3 -12
- package/engine/agents/claude-options.ts +3 -6
- package/engine/agents/launch-session.ts +5 -9
- package/engine/agents/role-effort.ts +55 -0
- package/engine/agents/scripted-launcher.ts +17 -3
- package/engine/config/config-fields.ts +3 -2
- package/engine/config/engine-config-schema.ts +42 -4
- package/engine/config/engine-config.ts +2 -0
- package/engine/core/agent-turn.ts +34 -11
- package/engine/core/execute-final-review.ts +11 -2
- package/engine/core/execute-memory.ts +7 -0
- package/engine/guards/helper-rules.ts +6 -0
- package/engine/index.ts +1 -1
- package/engine/jev/jev-jobs.ts +57 -10
- package/engine/jev/jev-schemas.ts +3 -1
- package/engine/journal/agent-entries.ts +5 -1
- package/engine/models/model-score-facts.ts +273 -0
- package/engine/models/model-score-rules.ts +35 -0
- package/engine/models/model-score.ts +169 -246
- package/engine/testing/effort-pick-fixtures.ts +139 -0
- package/engine/testing/model-score-fixtures.ts +300 -0
- package/package.json +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runOf,
|
|
3
|
+
ticketRun,
|
|
4
|
+
type Built,
|
|
5
|
+
type Effort,
|
|
6
|
+
type Severity,
|
|
7
|
+
} from './model-score-fixtures'
|
|
8
|
+
|
|
9
|
+
import { ModelsConfigSchema } from '../config/engine-config'
|
|
10
|
+
import type { JournalEntry, JournalRecord } from '../journal/journal-record'
|
|
11
|
+
import type { ModelReport } from '../models/model-score'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Journal builders for the score report's "Clef's effort picks" section
|
|
15
|
+
* (#663): Clef's shadow-mode `agent_effort` answers, the tickets they were
|
|
16
|
+
* asked on, and a run with its own confidence bar.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Clef's `agent_effort` answer about `role`. */
|
|
20
|
+
export type EffortPick = {
|
|
21
|
+
role: Built
|
|
22
|
+
pick: Effort
|
|
23
|
+
confidence: number | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An `agent_effort` ask about `role`, in shadow mode, and Clef's answer.
|
|
28
|
+
* `runOf` points the answer's `asked_seq` at the ask.
|
|
29
|
+
*/
|
|
30
|
+
export const effortAsk = ({
|
|
31
|
+
ticket,
|
|
32
|
+
role,
|
|
33
|
+
pick,
|
|
34
|
+
confidence,
|
|
35
|
+
}: EffortPick & { ticket: number }): JournalEntry[] => [
|
|
36
|
+
{
|
|
37
|
+
kind: 'jev_asked',
|
|
38
|
+
ticket,
|
|
39
|
+
role,
|
|
40
|
+
content: {
|
|
41
|
+
job: 'agent_effort',
|
|
42
|
+
request: {
|
|
43
|
+
state: {},
|
|
44
|
+
questions: {
|
|
45
|
+
effort: {
|
|
46
|
+
type: 'choice',
|
|
47
|
+
instructions:
|
|
48
|
+
'Which effort should this agent use? Pick the lowest that will get it right.',
|
|
49
|
+
criteria: { medium: null, high: null, xhigh: null },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
fixed: { effort: 'medium' },
|
|
54
|
+
acted: false,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
kind: 'jev_answered',
|
|
59
|
+
ticket,
|
|
60
|
+
role,
|
|
61
|
+
content: {
|
|
62
|
+
job: 'agent_effort',
|
|
63
|
+
asked_seq: 1,
|
|
64
|
+
answers: { effort: { value: pick, confidence, raw: {} } },
|
|
65
|
+
ms: 5,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* One ticket after Clef's effort `picks`: the test-writer at `tw_effort`
|
|
72
|
+
* and the implementer at `impl_effort` (both medium unless told), every
|
|
73
|
+
* ticket review at xhigh, the first review's `findings`, 100 lines.
|
|
74
|
+
*/
|
|
75
|
+
export const pickedTicket = ({
|
|
76
|
+
ticket,
|
|
77
|
+
tw_effort = 'medium',
|
|
78
|
+
impl_effort = 'medium',
|
|
79
|
+
picks = [],
|
|
80
|
+
findings = [],
|
|
81
|
+
}: {
|
|
82
|
+
ticket: number
|
|
83
|
+
tw_effort?: Effort
|
|
84
|
+
impl_effort?: Effort
|
|
85
|
+
picks?: EffortPick[]
|
|
86
|
+
findings?: Severity[]
|
|
87
|
+
}): JournalEntry[] => [
|
|
88
|
+
...picks.flatMap((pick) => effortAsk({ ticket, ...pick })),
|
|
89
|
+
...ticketRun({
|
|
90
|
+
ticket,
|
|
91
|
+
tw_effort,
|
|
92
|
+
impl_effort,
|
|
93
|
+
review_effort: 'xhigh',
|
|
94
|
+
findings,
|
|
95
|
+
}),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
/** A run of `body` whose config sets the confidence bar to `bar`. */
|
|
99
|
+
export const runWithBar = (
|
|
100
|
+
body: JournalEntry[],
|
|
101
|
+
bar: number
|
|
102
|
+
): JournalRecord[] =>
|
|
103
|
+
runOf(body).map((record) =>
|
|
104
|
+
record.kind === 'run_started'
|
|
105
|
+
? {
|
|
106
|
+
...record,
|
|
107
|
+
content: {
|
|
108
|
+
...record.content,
|
|
109
|
+
config: {
|
|
110
|
+
...record.content.config,
|
|
111
|
+
models: ModelsConfigSchema.parse({
|
|
112
|
+
confidence_bar: bar,
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: record
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
/** The report's "Clef's effort picks" for `role`. */
|
|
121
|
+
export const effortPicksOf = (report: ModelReport, role: Built) =>
|
|
122
|
+
report.effort_picks.find((picks) => picks.role === role)
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The lines of `role`'s "Clef's effort picks (shadow mode)" section in the
|
|
126
|
+
* printed report: the lines after its heading, within the role's part.
|
|
127
|
+
*/
|
|
128
|
+
export const pickSection = (text: string, role: Built): string[] => {
|
|
129
|
+
const lines = text.split('\n')
|
|
130
|
+
const start = lines.indexOf(role)
|
|
131
|
+
const end = lines.findIndex(
|
|
132
|
+
(line, at) => at > start && line !== '' && !line.startsWith(' ')
|
|
133
|
+
)
|
|
134
|
+
const part = start < 0 ? [] : lines.slice(start + 1, end)
|
|
135
|
+
const heading = part.findIndex(
|
|
136
|
+
(line) => line.trim() === "Clef's effort picks (shadow mode)"
|
|
137
|
+
)
|
|
138
|
+
return heading < 0 ? [] : part.slice(heading + 1)
|
|
139
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentFailed,
|
|
3
|
+
agentSession,
|
|
4
|
+
finding,
|
|
5
|
+
implemented,
|
|
6
|
+
intakePassed,
|
|
7
|
+
practiceTicket,
|
|
8
|
+
pushed,
|
|
9
|
+
reviewed,
|
|
10
|
+
testsWritten,
|
|
11
|
+
} from './build-fixtures'
|
|
12
|
+
import { recordsFrom } from './intake-fixtures'
|
|
13
|
+
|
|
14
|
+
import type { AgentRole } from '../agents/role-results'
|
|
15
|
+
import type { JournalEntry, JournalRecord } from '../journal/journal-record'
|
|
16
|
+
import type { ModelScore } from '../models/model-score'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Journal builders for the model score report's tests (#605): a ticket
|
|
20
|
+
* built with each role on its model and effort, a run's records, and
|
|
21
|
+
* lookups into the report's scores.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type Model = 'opus' | 'sonnet' | 'haiku'
|
|
25
|
+
export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
|
|
26
|
+
export type Severity = 'blocker' | 'should_fix' | 'nit'
|
|
27
|
+
export type Built = 'test-writer' | 'implementer'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An agent turn started on `model`; `null` writes one from before #600. It
|
|
31
|
+
* ran at `effort`; left out writes one from before #659, with no effort.
|
|
32
|
+
*/
|
|
33
|
+
export const started = ({
|
|
34
|
+
ticket,
|
|
35
|
+
role,
|
|
36
|
+
model,
|
|
37
|
+
follow_up_of,
|
|
38
|
+
effort,
|
|
39
|
+
}: {
|
|
40
|
+
ticket: number | null
|
|
41
|
+
role: AgentRole
|
|
42
|
+
model: Model | null
|
|
43
|
+
follow_up_of?: string
|
|
44
|
+
effort?: Effort
|
|
45
|
+
}): JournalEntry => ({
|
|
46
|
+
kind: 'agent_started',
|
|
47
|
+
ticket,
|
|
48
|
+
role,
|
|
49
|
+
content: {
|
|
50
|
+
role,
|
|
51
|
+
prompt: 'p',
|
|
52
|
+
follow_up_of: follow_up_of ?? null,
|
|
53
|
+
...(model === null ? {} : { model }),
|
|
54
|
+
...(effort === undefined ? {} : { effort }),
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An `agent_model` ask about `role` and Clef's answer, or with `role` null
|
|
60
|
+
* an older journal's `ticket_model` ask, which named full model ids.
|
|
61
|
+
* `runOf` points the answer's `asked_seq` at the ask.
|
|
62
|
+
*/
|
|
63
|
+
export const modelAsk = ({
|
|
64
|
+
ticket,
|
|
65
|
+
role,
|
|
66
|
+
pick,
|
|
67
|
+
confidence,
|
|
68
|
+
acted,
|
|
69
|
+
}: {
|
|
70
|
+
ticket: number | null
|
|
71
|
+
role: Built | 'learner' | null
|
|
72
|
+
pick: string
|
|
73
|
+
confidence: number | null
|
|
74
|
+
acted?: boolean
|
|
75
|
+
}): JournalEntry[] => {
|
|
76
|
+
const job = role === null ? 'ticket_model' : 'agent_model'
|
|
77
|
+
const options =
|
|
78
|
+
role === null
|
|
79
|
+
? ['claude-opus-5-5', 'claude-sonnet-5', 'claude-haiku-4-5']
|
|
80
|
+
: ['opus', 'sonnet', 'haiku']
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
kind: 'jev_asked',
|
|
84
|
+
ticket,
|
|
85
|
+
role,
|
|
86
|
+
content: {
|
|
87
|
+
job,
|
|
88
|
+
request: {
|
|
89
|
+
state: {},
|
|
90
|
+
questions: {
|
|
91
|
+
model: {
|
|
92
|
+
type: 'choice',
|
|
93
|
+
instructions: 'Which model?',
|
|
94
|
+
criteria: Object.fromEntries(
|
|
95
|
+
options.map((option) => [option, null])
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
fixed: { model: options[0] ?? 'opus' },
|
|
101
|
+
...(acted === undefined ? {} : { acted }),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
kind: 'jev_answered',
|
|
106
|
+
ticket,
|
|
107
|
+
role,
|
|
108
|
+
content: {
|
|
109
|
+
job,
|
|
110
|
+
asked_seq: 1,
|
|
111
|
+
answers: { model: { value: pick, confidence, raw: {} } },
|
|
112
|
+
ms: 5,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type Ask = Omit<Parameters<typeof modelAsk>[0], 'ticket'>
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* One ticket built: the test-writer on `tw`, the implementer on `impl`
|
|
122
|
+
* (opus unless told), the first review's `findings`, then `rounds` fix
|
|
123
|
+
* rounds (one when the findings block), each fix's review finding a
|
|
124
|
+
* should-fix until the last approves, and a join of `lines` lines. Each
|
|
125
|
+
* role's agents run at its effort, left out as before #659.
|
|
126
|
+
*/
|
|
127
|
+
export const ticketRun = ({
|
|
128
|
+
ticket,
|
|
129
|
+
tw = 'opus',
|
|
130
|
+
impl = 'opus',
|
|
131
|
+
tw_effort,
|
|
132
|
+
impl_effort,
|
|
133
|
+
review_effort,
|
|
134
|
+
asks = [],
|
|
135
|
+
findings = [],
|
|
136
|
+
rounds,
|
|
137
|
+
lines = 100,
|
|
138
|
+
tokens = 100,
|
|
139
|
+
tw_failed_first = false,
|
|
140
|
+
}: {
|
|
141
|
+
ticket: number
|
|
142
|
+
tw?: Model | null
|
|
143
|
+
impl?: Model | null
|
|
144
|
+
tw_effort?: Effort
|
|
145
|
+
impl_effort?: Effort
|
|
146
|
+
review_effort?: Effort
|
|
147
|
+
asks?: Ask[]
|
|
148
|
+
findings?: Severity[]
|
|
149
|
+
rounds?: number
|
|
150
|
+
/** `null` joins as an older journal does, with no `lines_changed`. */
|
|
151
|
+
lines?: number | null
|
|
152
|
+
/** The ticket's tokens, all in its test-writer's session. */
|
|
153
|
+
tokens?: number
|
|
154
|
+
/** The test-writer on `tw` fails a try; a fresh one on opus follows. */
|
|
155
|
+
tw_failed_first?: boolean
|
|
156
|
+
}): JournalEntry[] => {
|
|
157
|
+
const askFor = (role: Built | null) =>
|
|
158
|
+
asks
|
|
159
|
+
.filter((ask) => ask.role === role)
|
|
160
|
+
.flatMap((ask) => modelAsk({ ticket, ...ask }))
|
|
161
|
+
const fixes =
|
|
162
|
+
rounds ?? (findings.some((severity) => severity !== 'nit') ? 1 : 0)
|
|
163
|
+
const review = (list: Severity[], index: number): JournalEntry[] => [
|
|
164
|
+
started({
|
|
165
|
+
ticket,
|
|
166
|
+
role: 'ticket-reviewer',
|
|
167
|
+
model: 'opus',
|
|
168
|
+
effort: review_effort,
|
|
169
|
+
}),
|
|
170
|
+
reviewed({
|
|
171
|
+
ticket,
|
|
172
|
+
findings: list.map((severity, at) =>
|
|
173
|
+
finding({ id: `F${index}-${at}`, severity })
|
|
174
|
+
),
|
|
175
|
+
}),
|
|
176
|
+
]
|
|
177
|
+
return [
|
|
178
|
+
...askFor(null),
|
|
179
|
+
...askFor('test-writer'),
|
|
180
|
+
...(tw_failed_first
|
|
181
|
+
? [
|
|
182
|
+
started({
|
|
183
|
+
ticket,
|
|
184
|
+
role: 'test-writer',
|
|
185
|
+
model: tw,
|
|
186
|
+
effort: tw_effort,
|
|
187
|
+
}),
|
|
188
|
+
agentFailed({
|
|
189
|
+
ticket,
|
|
190
|
+
role: 'test-writer',
|
|
191
|
+
failure: 'agent',
|
|
192
|
+
}),
|
|
193
|
+
]
|
|
194
|
+
: []),
|
|
195
|
+
started({
|
|
196
|
+
ticket,
|
|
197
|
+
role: 'test-writer',
|
|
198
|
+
model: tw_failed_first ? 'opus' : tw,
|
|
199
|
+
effort: tw_effort,
|
|
200
|
+
}),
|
|
201
|
+
testsWritten({ ticket }),
|
|
202
|
+
agentSession({
|
|
203
|
+
ticket,
|
|
204
|
+
role: 'test-writer',
|
|
205
|
+
output_tokens: tokens - 10,
|
|
206
|
+
}),
|
|
207
|
+
...askFor('implementer'),
|
|
208
|
+
started({
|
|
209
|
+
ticket,
|
|
210
|
+
role: 'implementer',
|
|
211
|
+
model: impl,
|
|
212
|
+
effort: impl_effort,
|
|
213
|
+
}),
|
|
214
|
+
implemented({ ticket }),
|
|
215
|
+
...review(findings, 0),
|
|
216
|
+
...Array.from({ length: fixes }, (_, index) => [
|
|
217
|
+
started({
|
|
218
|
+
ticket,
|
|
219
|
+
role: 'implementer',
|
|
220
|
+
model: impl,
|
|
221
|
+
follow_up_of: 'impl-1',
|
|
222
|
+
effort: impl_effort,
|
|
223
|
+
}),
|
|
224
|
+
implemented({ ticket }),
|
|
225
|
+
...review(index + 1 < fixes ? ['should_fix'] : [], index + 1),
|
|
226
|
+
]).flat(),
|
|
227
|
+
{
|
|
228
|
+
kind: 'ticket_joined',
|
|
229
|
+
ticket,
|
|
230
|
+
role: null,
|
|
231
|
+
content: {
|
|
232
|
+
ok: true,
|
|
233
|
+
shas: ['r1', 'g1'],
|
|
234
|
+
...(lines === null ? {} : { lines_changed: lines }),
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
pushed({ ticket }),
|
|
238
|
+
{
|
|
239
|
+
kind: 'usage_recorded',
|
|
240
|
+
ticket,
|
|
241
|
+
role: null,
|
|
242
|
+
content: {
|
|
243
|
+
scope: 'ticket',
|
|
244
|
+
ticket,
|
|
245
|
+
agent_turns: 1,
|
|
246
|
+
tokens: {
|
|
247
|
+
input_tokens: 10,
|
|
248
|
+
output_tokens: tokens - 10,
|
|
249
|
+
cache_read_input_tokens: 0,
|
|
250
|
+
cache_creation_input_tokens: 0,
|
|
251
|
+
},
|
|
252
|
+
windows: {},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A run's records: its intake, then `body`, each answer after its ask. */
|
|
259
|
+
export const runOf = (body: JournalEntry[]): JournalRecord[] => {
|
|
260
|
+
const numbers = [
|
|
261
|
+
...new Set(
|
|
262
|
+
body.flatMap(({ ticket }) => (ticket === null ? [] : [ticket]))
|
|
263
|
+
),
|
|
264
|
+
]
|
|
265
|
+
const entries = [
|
|
266
|
+
...intakePassed({
|
|
267
|
+
tickets: numbers.map((number) => practiceTicket({ number })),
|
|
268
|
+
}),
|
|
269
|
+
...body,
|
|
270
|
+
]
|
|
271
|
+
return recordsFrom({
|
|
272
|
+
entries: entries.map((entry, index) =>
|
|
273
|
+
entry.kind === 'jev_answered'
|
|
274
|
+
? { ...entry, content: { ...entry.content, asked_seq: index } }
|
|
275
|
+
: entry
|
|
276
|
+
),
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const scoreOf = (
|
|
281
|
+
scores: ModelScore[],
|
|
282
|
+
role: Built,
|
|
283
|
+
model: Model
|
|
284
|
+
): ModelScore | undefined =>
|
|
285
|
+
scores.find((score) => score.role === role && score.model === model)
|
|
286
|
+
|
|
287
|
+
/** The role and model of each score with tickets, sorted. */
|
|
288
|
+
export const groups = (scores: ModelScore[]): string[] =>
|
|
289
|
+
scores
|
|
290
|
+
.filter(({ tickets }) => tickets > 0)
|
|
291
|
+
.map(({ role, model }) => `${role}/${model}`)
|
|
292
|
+
.sort()
|
|
293
|
+
|
|
294
|
+
/** Clef picked `pick` for the test-writer, sure enough to act on it. */
|
|
295
|
+
export const sure = (pick: Model, confidence = 0.9): Ask => ({
|
|
296
|
+
role: 'test-writer',
|
|
297
|
+
pick,
|
|
298
|
+
confidence,
|
|
299
|
+
acted: true,
|
|
300
|
+
})
|
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.22",
|
|
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",
|