@alecsibilia/luca 14.0.0-alpha.16 → 14.0.0-alpha.17
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 +233 -0
- package/engine/agents/claude-launcher.ts +23 -415
- package/engine/agents/claude-messages.ts +190 -0
- package/engine/agents/claude-options.ts +34 -7
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/open-session.ts +197 -0
- package/engine/agents/scripted-launcher.ts +17 -6
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/config/config-reload.ts +3 -0
- package/engine/config/engine-config.ts +59 -1
- package/engine/core/agent-turn.ts +100 -3
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/execute-final-review.ts +28 -3
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-with-jev.ts +14 -5
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/git/git-adapter.ts +14 -473
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/index.ts +2 -1
- package/engine/jev/jev-jobs.ts +84 -32
- package/engine/jev/jev-schemas.ts +6 -1
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +11 -3
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +8 -1
- package/engine/journal/agent-turn-replay.ts +8 -0
- package/engine/journal/decision-model-entries.ts +4 -1
- package/engine/journal/join-entries.ts +5 -0
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/memory-replay.ts +14 -0
- package/engine/journal/ticket-progress.ts +13 -0
- package/engine/journal/ticket-replay.ts +10 -5
- package/engine/journal/ticket-result-replay.ts +26 -4
- package/engine/models/model-score.ts +512 -0
- package/engine/testing/agent-turn-fixtures.ts +279 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -1045
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- 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.17"
|
|
@@ -157,12 +157,17 @@ export type AgentLauncher = {
|
|
|
157
157
|
* for its follow-ups.
|
|
158
158
|
*/
|
|
159
159
|
messaging: AgentMessaging | null
|
|
160
|
+
/**
|
|
161
|
+
* The model's short name: `opus`, `sonnet` or `haiku`. The session's
|
|
162
|
+
* follow-ups run on it too.
|
|
163
|
+
*/
|
|
164
|
+
model: string
|
|
160
165
|
}) => Promise<AgentTurn>
|
|
161
166
|
/**
|
|
162
167
|
* Sends a follow-up message to an agent session that is still open, such
|
|
163
168
|
* as a failed gate's output in a fix loop, and returns the session's new
|
|
164
|
-
* result. The agent keeps everything it knew from its earlier turns,
|
|
165
|
-
*
|
|
169
|
+
* result. The agent keeps everything it knew from its earlier turns, the
|
|
170
|
+
* guards it was launched with, and its model. A session the launcher doesn't know,
|
|
166
171
|
* or one that has closed, is an engine failure.
|
|
167
172
|
*
|
|
168
173
|
* @example
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { $ } from 'bun'
|
|
6
|
+
|
|
7
|
+
import type { Options, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
|
|
8
|
+
import { afterEach, beforeEach } from 'bun:test'
|
|
9
|
+
|
|
10
|
+
import type { AgentMessaging, AgentTurn } from './agent-launcher'
|
|
11
|
+
import { createClaudeLauncher, type AgentQuery } from './claude-launcher'
|
|
12
|
+
import { CLAUDE_MODEL } from './claude-options'
|
|
13
|
+
import type { AgentRole } from './role-results'
|
|
14
|
+
|
|
15
|
+
import type { EngineConfig } from '../config/engine-config'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helpers shared by the launcher tests (`claude-launcher.test.ts` and the
|
|
19
|
+
* `claude-launcher-*.test.ts` files beside it).
|
|
20
|
+
*
|
|
21
|
+
* The launcher seam: `createClaudeLauncher` with a fake `query` that plays
|
|
22
|
+
* back SDK-shaped messages, and a fake `claude` path. No model, no process.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const CONFIG: EngineConfig = {
|
|
26
|
+
checks: { test: 'bun test', types: 'bunx --bun tsc --noEmit' },
|
|
27
|
+
test_file_patterns: ['src/**/*.test.ts'],
|
|
28
|
+
test_setup_files: ['src/test-setup.ts'],
|
|
29
|
+
rule_files: [],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const CLAUDE_PATH = '/opt/fake/bin/claude'
|
|
33
|
+
|
|
34
|
+
export const MAX_ACCOUNT = {
|
|
35
|
+
email: 'me@example.com',
|
|
36
|
+
subscriptionType: 'max',
|
|
37
|
+
apiKeySource: 'none',
|
|
38
|
+
apiProvider: 'firstParty',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const INIT = {
|
|
42
|
+
type: 'system',
|
|
43
|
+
subtype: 'init',
|
|
44
|
+
apiKeySource: 'none',
|
|
45
|
+
model: 'claude-opus-5-5',
|
|
46
|
+
claude_code_version: '2.1.280',
|
|
47
|
+
session_id: 'session-1',
|
|
48
|
+
cwd: '/somewhere',
|
|
49
|
+
tools: ['Read', 'Grep', 'Glob', 'Bash', 'StructuredOutput'],
|
|
50
|
+
mcp_servers: [],
|
|
51
|
+
permissionMode: 'dontAsk',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const APPROVE = {
|
|
55
|
+
verdict: 'approve',
|
|
56
|
+
findings: [],
|
|
57
|
+
summary: 'Fine.',
|
|
58
|
+
assumptions: [],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const result = (fields: Record<string, unknown>) => ({
|
|
62
|
+
type: 'result',
|
|
63
|
+
subtype: 'success',
|
|
64
|
+
is_error: false,
|
|
65
|
+
num_turns: 3,
|
|
66
|
+
duration_ms: 1200,
|
|
67
|
+
total_cost_usd: 0.25,
|
|
68
|
+
usage: {
|
|
69
|
+
input_tokens: 10,
|
|
70
|
+
output_tokens: 20,
|
|
71
|
+
cache_read_input_tokens: 30,
|
|
72
|
+
cache_creation_input_tokens: 40,
|
|
73
|
+
},
|
|
74
|
+
permission_denials: [],
|
|
75
|
+
session_id: 'session-1',
|
|
76
|
+
...fields,
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
type FakeCall = {
|
|
80
|
+
options: Options
|
|
81
|
+
prompts: SDKUserMessage[]
|
|
82
|
+
account_checked_before_prompt: boolean
|
|
83
|
+
interrupted: boolean
|
|
84
|
+
closed: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A fake `query`: it answers `accountInfo()` with `account`, waits for the
|
|
89
|
+
* first prompt, then plays `messages` (a function throws where it sits).
|
|
90
|
+
* With `hang`, it runs until it is closed. With `turns`, it plays one list
|
|
91
|
+
* per prompt instead, like a session in streaming-input mode, and runs until
|
|
92
|
+
* it is closed. `during` runs after each prompt arrives and before that
|
|
93
|
+
* turn's messages play, the way the agent's tool calls happen mid-turn.
|
|
94
|
+
*/
|
|
95
|
+
export const fakeQuery = ({
|
|
96
|
+
account,
|
|
97
|
+
messages,
|
|
98
|
+
hang,
|
|
99
|
+
turns,
|
|
100
|
+
during,
|
|
101
|
+
}: {
|
|
102
|
+
account?: unknown
|
|
103
|
+
messages?: (unknown | (() => never))[]
|
|
104
|
+
hang?: boolean
|
|
105
|
+
turns?: unknown[][]
|
|
106
|
+
during?: (call: FakeCall, turn: number) => Promise<void>
|
|
107
|
+
}) => {
|
|
108
|
+
const calls: FakeCall[] = []
|
|
109
|
+
const query: AgentQuery = ({ prompt, options }) => {
|
|
110
|
+
let accountChecked = false
|
|
111
|
+
const call: FakeCall = {
|
|
112
|
+
options,
|
|
113
|
+
prompts: [],
|
|
114
|
+
account_checked_before_prompt: false,
|
|
115
|
+
interrupted: false,
|
|
116
|
+
closed: false,
|
|
117
|
+
}
|
|
118
|
+
calls.push(call)
|
|
119
|
+
let wake = () => {}
|
|
120
|
+
const woken = new Promise<void>((resolve) => {
|
|
121
|
+
wake = resolve
|
|
122
|
+
})
|
|
123
|
+
let end = () => {}
|
|
124
|
+
const ended = new Promise<void>((resolve) => {
|
|
125
|
+
end = resolve
|
|
126
|
+
})
|
|
127
|
+
let notify = () => {}
|
|
128
|
+
const prompted = (count: number) =>
|
|
129
|
+
new Promise<void>((resolve) => {
|
|
130
|
+
notify = () => {
|
|
131
|
+
if (call.prompts.length >= count || call.closed) resolve()
|
|
132
|
+
}
|
|
133
|
+
notify()
|
|
134
|
+
})
|
|
135
|
+
void (async () => {
|
|
136
|
+
for await (const message of prompt) {
|
|
137
|
+
if (call.prompts.length === 0) {
|
|
138
|
+
call.account_checked_before_prompt = accountChecked
|
|
139
|
+
}
|
|
140
|
+
call.prompts.push(message)
|
|
141
|
+
wake()
|
|
142
|
+
notify()
|
|
143
|
+
}
|
|
144
|
+
})()
|
|
145
|
+
return {
|
|
146
|
+
[Symbol.asyncIterator]: async function* () {
|
|
147
|
+
if (turns !== undefined) {
|
|
148
|
+
for (const [index, batch] of turns.entries()) {
|
|
149
|
+
await prompted(index + 1)
|
|
150
|
+
if (call.closed) return
|
|
151
|
+
await during?.(call, index)
|
|
152
|
+
for (const message of batch) yield message
|
|
153
|
+
}
|
|
154
|
+
await ended
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
await woken
|
|
158
|
+
if (call.closed) return
|
|
159
|
+
await during?.(call, 0)
|
|
160
|
+
for (const message of messages ?? []) {
|
|
161
|
+
if (typeof message === 'function') message()
|
|
162
|
+
yield message
|
|
163
|
+
}
|
|
164
|
+
if (hang) await ended
|
|
165
|
+
},
|
|
166
|
+
accountInfo: async () => {
|
|
167
|
+
accountChecked = true
|
|
168
|
+
return account ?? MAX_ACCOUNT
|
|
169
|
+
},
|
|
170
|
+
interrupt: async () => {
|
|
171
|
+
call.interrupted = true
|
|
172
|
+
},
|
|
173
|
+
close: () => {
|
|
174
|
+
call.closed = true
|
|
175
|
+
wake()
|
|
176
|
+
end()
|
|
177
|
+
notify()
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { query, calls }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The git repo the current test runs in: a fresh one for each test. */
|
|
185
|
+
export let repo = ''
|
|
186
|
+
|
|
187
|
+
/** Gives each test of the file that calls it a fresh git repo in `repo`. */
|
|
188
|
+
export const repoPerTest = () => {
|
|
189
|
+
beforeEach(async () => {
|
|
190
|
+
repo = await mkdtemp(join(tmpdir(), 'luca-launcher-'))
|
|
191
|
+
await $`git init -q -b main ${repo}`.quiet()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
afterEach(async () => {
|
|
195
|
+
await rm(repo, { recursive: true, force: true })
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const launch = ({
|
|
200
|
+
query,
|
|
201
|
+
model,
|
|
202
|
+
role,
|
|
203
|
+
turn_timeout_ms,
|
|
204
|
+
messaging,
|
|
205
|
+
}: {
|
|
206
|
+
query: AgentQuery
|
|
207
|
+
model?: string
|
|
208
|
+
role?: AgentRole
|
|
209
|
+
turn_timeout_ms?: number
|
|
210
|
+
messaging?: AgentMessaging
|
|
211
|
+
}): Promise<AgentTurn> =>
|
|
212
|
+
createClaudeLauncher({
|
|
213
|
+
query,
|
|
214
|
+
claude_path: CLAUDE_PATH,
|
|
215
|
+
turn_timeout_ms,
|
|
216
|
+
}).launch({
|
|
217
|
+
role: role ?? 'ticket-reviewer',
|
|
218
|
+
ticket: 11,
|
|
219
|
+
prompt: 'Review ticket #11.',
|
|
220
|
+
cwd: repo,
|
|
221
|
+
may_edit_tests: role === 'test-writer',
|
|
222
|
+
config: CONFIG,
|
|
223
|
+
messaging: messaging ?? null,
|
|
224
|
+
model: model ?? CLAUDE_MODEL,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
export const IMPLEMENTER_DONE = {
|
|
228
|
+
outcome: 'done',
|
|
229
|
+
bad_test: null,
|
|
230
|
+
summary: 'Done.',
|
|
231
|
+
assumptions: [],
|
|
232
|
+
run_notes: [],
|
|
233
|
+
}
|