@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/board/package.json +1 -1
  3. package/board/server/luca-version.ts +1 -1
  4. package/engine/agents/agent-launcher.ts +7 -2
  5. package/engine/agents/claude-launcher-helpers.ts +233 -0
  6. package/engine/agents/claude-launcher.ts +23 -415
  7. package/engine/agents/claude-messages.ts +190 -0
  8. package/engine/agents/claude-options.ts +34 -7
  9. package/engine/agents/input-channel.ts +36 -0
  10. package/engine/agents/launch-paths.ts +29 -0
  11. package/engine/agents/open-session.ts +197 -0
  12. package/engine/agents/scripted-launcher.ts +17 -6
  13. package/engine/cli/luca.ts +8 -1
  14. package/engine/cli/models-command.ts +119 -0
  15. package/engine/config/config-reload.ts +3 -0
  16. package/engine/config/engine-config.ts +59 -1
  17. package/engine/core/agent-turn.ts +100 -3
  18. package/engine/core/decide-build-helpers.ts +87 -0
  19. package/engine/core/execute-final-review.ts +28 -3
  20. package/engine/core/execute-memory.ts +12 -0
  21. package/engine/core/execute-with-jev.ts +14 -5
  22. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  23. package/engine/core/run-session-close-helpers.ts +96 -0
  24. package/engine/git/git-adapter.ts +14 -473
  25. package/engine/git/git-commits.ts +132 -0
  26. package/engine/git/git-join.ts +139 -0
  27. package/engine/git/git-read.ts +123 -0
  28. package/engine/git/git-run.ts +32 -0
  29. package/engine/git/git-worktrees.ts +143 -0
  30. package/engine/index.ts +2 -1
  31. package/engine/jev/jev-jobs.ts +84 -32
  32. package/engine/jev/jev-schemas.ts +6 -1
  33. package/engine/jev/jev-shadow-helpers.ts +218 -0
  34. package/engine/jev/jev-shadow.ts +11 -3
  35. package/engine/jev/model-pick.ts +275 -0
  36. package/engine/journal/agent-entries.ts +8 -1
  37. package/engine/journal/agent-turn-replay.ts +8 -0
  38. package/engine/journal/decision-model-entries.ts +4 -1
  39. package/engine/journal/join-entries.ts +5 -0
  40. package/engine/journal/journal.ts +6 -1
  41. package/engine/journal/memory-replay.ts +14 -0
  42. package/engine/journal/ticket-progress.ts +13 -0
  43. package/engine/journal/ticket-replay.ts +10 -5
  44. package/engine/journal/ticket-result-replay.ts +26 -4
  45. package/engine/models/model-score.ts +512 -0
  46. package/engine/testing/agent-turn-fixtures.ts +279 -0
  47. package/engine/testing/already-done-fixtures.ts +97 -0
  48. package/engine/testing/build-fixtures.ts +68 -1045
  49. package/engine/testing/limit-fixtures.ts +85 -0
  50. package/engine/testing/reply-fixtures.ts +88 -0
  51. package/engine/testing/run-fixtures.ts +219 -0
  52. package/engine/testing/step-record-fixtures.ts +55 -0
  53. package/engine/testing/ticket-step-fixtures.ts +272 -0
  54. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @alecsibilia/luca
2
2
 
3
+ ## 14.0.0-alpha.17
4
+
5
+ No changes in this release.
6
+
3
7
  ## 14.0.0-alpha.16
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luca/board",
3
- "version": "14.0.0-alpha.16",
3
+ "version": "14.0.0-alpha.17",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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.16"
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, and
165
- * the guards it was launched with. A session the launcher doesn't know,
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
+ }