@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

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 (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -1,53 +1,15 @@
1
- import { randomUUID } from 'node:crypto'
2
- import { realpathSync } from 'node:fs'
3
- import { homedir } from 'node:os'
4
-
5
1
  import {
6
2
  query as sdkQuery,
7
3
  type EffortLevel,
8
- type Options,
9
- type SDKUserMessage,
10
4
  } from '@anthropic-ai/claude-agent-sdk'
11
- import { z } from 'zod'
12
5
 
13
- import {
14
- AgentSessionSchema,
15
- type AgentLauncher,
16
- type AgentSession,
17
- type AgentTurn,
18
- type ModelTokens,
19
- } from './agent-launcher'
20
- import {
21
- AGENT_EFFORT,
22
- agentEnv,
23
- agentOptions,
24
- CLAUDE_MODEL,
25
- checkModel,
26
- } from './claude-options'
27
- import { createDeliveryHook, createLucaServer } from './message-tool'
28
- import type { AgentRole } from './role-results'
6
+ import type { AgentLauncher } from './agent-launcher'
7
+ import { AGENT_EFFORT } from './claude-options'
8
+ import { createLaunch, type AgentQuery } from './launch-session'
9
+ import { createSessionPool } from './session-pool'
29
10
 
30
- import { createGuardHook } from '../guards/guard-hook'
31
- import { guardRoleOf } from '../guards/role-rules'
32
- import { planSignal } from '../limits/plan-signals'
33
- import { runCommand } from '../shell/run-command'
34
-
35
- /**
36
- * One running SDK session, as the launcher uses it. The SDK's own `Query`
37
- * fits this; tests hand in a fake. Messages are read as `unknown` and parsed
38
- * with the launcher's own schemas.
39
- */
40
- export type AgentQuerySession = AsyncIterable<unknown> & {
41
- accountInfo: () => Promise<unknown>
42
- interrupt: () => Promise<unknown>
43
- close: () => void
44
- }
45
-
46
- /** Starts an SDK session: the SDK's `query`, or a fake in tests. */
47
- export type AgentQuery = (params: {
48
- prompt: AsyncIterable<SDKUserMessage>
49
- options: Options
50
- }) => AgentQuerySession
11
+ export type { AgentQuery } from './launch-session'
12
+ export type { AgentQuerySession } from './open-session'
51
13
 
52
14
  /** A turn gives up after this long unless told otherwise: 40 minutes. */
53
15
  export const DEFAULT_TURN_TIMEOUT_MS = 40 * 60_000
@@ -58,401 +20,12 @@ export const DEFAULT_TURN_TIMEOUT_MS = 40 * 60_000
58
20
  */
59
21
  export const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60_000
60
22
 
61
- /** The Claude plans an agent may run on. */
62
- const PLANS = ['pro', 'max', 'team', 'enterprise']
63
-
64
- const AccountSchema = z.looseObject({
65
- subscriptionType: z.string().optional(),
66
- apiKeySource: z.string().optional(),
67
- apiProvider: z.string().optional(),
68
- })
69
-
70
- const InitSchema = z.object({
71
- type: z.literal('system'),
72
- subtype: z.literal('init'),
73
- apiKeySource: z.string(),
74
- model: z.string(),
75
- claude_code_version: z.string().nullable().default(null),
76
- session_id: z.string().nullable().default(null),
77
- tools: z.array(z.string()).default([]),
78
- mcp_servers: z.array(z.object({ name: z.string() })).default([]),
79
- })
80
-
81
- const RateLimitSchema = z.object({
82
- type: z.literal('rate_limit_event'),
83
- rate_limit_info: z.record(z.string(), z.unknown()),
84
- })
85
-
86
- const AssistantSchema = z.object({
87
- type: z.literal('assistant'),
88
- error: z.string().optional(),
89
- })
90
-
91
- const ResultSchema = z.object({
92
- type: z.literal('result'),
93
- subtype: z.string(),
94
- is_error: z.boolean().default(false),
95
- num_turns: z.number().int().min(0).default(0),
96
- duration_ms: z.number().min(0).default(0),
97
- total_cost_usd: z.number().default(0),
98
- usage: z
99
- .object({
100
- input_tokens: z.number().default(0),
101
- output_tokens: z.number().default(0),
102
- cache_read_input_tokens: z.number().default(0),
103
- cache_creation_input_tokens: z.number().default(0),
104
- })
105
- .default({
106
- input_tokens: 0,
107
- output_tokens: 0,
108
- cache_read_input_tokens: 0,
109
- cache_creation_input_tokens: 0,
110
- }),
111
- /** Tokens per model, subagents included: the session's running totals. */
112
- modelUsage: z
113
- .record(
114
- z.string(),
115
- z.object({
116
- inputTokens: z.number().default(0),
117
- outputTokens: z.number().default(0),
118
- cacheReadInputTokens: z.number().default(0),
119
- cacheCreationInputTokens: z.number().default(0),
120
- })
121
- )
122
- .default({}),
123
- permission_denials: z
124
- .array(z.object({ tool_name: z.string(), tool_use_id: z.string() }))
125
- .default([]),
126
- structured_output: z.unknown().optional(),
127
- result: z.string().default(''),
128
- errors: z.array(z.string()).default([]),
129
- session_id: z.string().optional(),
130
- })
131
-
132
- type Failed = {
133
- ok: false
134
- failure: 'agent' | 'engine' | 'stop' | 'plan'
135
- error: string
136
- }
137
-
138
- type Outcome = { ok: true; structured_output: unknown } | Failed
139
-
140
- const stop = (error: string): Failed => ({ ok: false, failure: 'stop', error })
141
-
142
- const errorText = (error: unknown): string =>
143
- error instanceof Error ? error.message : String(error)
144
-
145
- /** Why the account may not run agents, or `null` if it may. */
146
- const accountProblem = (raw: unknown): string | null => {
147
- const parsed = AccountSchema.safeParse(raw)
148
- if (!parsed.success)
149
- return 'accountInfo() gave an answer of the wrong shape'
150
- const { subscriptionType, apiKeySource, apiProvider } = parsed.data
151
- const plan = (subscriptionType ?? '').toLowerCase()
152
- if (!PLANS.some((name) => plan.includes(name))) {
153
- return `no Claude subscription (subscriptionType=${subscriptionType ?? 'none'})`
154
- }
155
- if (apiKeySource !== undefined && apiKeySource !== 'none') {
156
- return `an API key is in use (apiKeySource=${apiKeySource})`
157
- }
158
- if (apiProvider !== undefined && apiProvider !== 'firstParty') {
159
- return `not Anthropic's own API (apiProvider=${apiProvider})`
160
- }
161
- return null
162
- }
163
-
164
- /** Why an init message is unsafe to go on with, or `null` if it is fine. */
165
- const initProblem = (init: z.infer<typeof InitSchema>): string | null => {
166
- if (init.apiKeySource !== 'none') {
167
- return `init apiKeySource=${init.apiKeySource} (expected "none")`
168
- }
169
- const model = checkModel({ model: init.model })
170
- if (model !== null) return `init model: ${model}`
171
- const servers = init.mcp_servers
172
- .map(({ name }) => name)
173
- .filter((name) => name !== 'luca')
174
- const tools = init.tools.filter(
175
- (tool) => tool.startsWith('mcp__') && !tool.startsWith('mcp__luca__')
176
- )
177
- if (servers.length > 0 || tools.length > 0) {
178
- return `foreign MCP servers or tools: ${[...servers, ...tools].join(', ')}`
179
- }
180
- return null
181
- }
182
-
183
- /** A turn the plan cut off: the reason, in words. */
184
- const planCut = (error: string): Failed => ({
185
- ok: false,
186
- failure: 'plan',
187
- error,
188
- })
189
-
190
- /** A queue the SDK reads the agent's prompt from (streaming-input mode). */
191
- const createInputChannel = () => {
192
- const queue: SDKUserMessage[] = []
193
- let wake: (() => void) | null = null
194
- let closed = false
195
- const poke = () => {
196
- wake?.()
197
- wake = null
198
- }
199
- const iterate = async function* (): AsyncGenerator<SDKUserMessage> {
200
- while (true) {
201
- const next = queue.shift()
202
- if (next !== undefined) {
203
- yield next
204
- continue
205
- }
206
- if (closed) return
207
- await new Promise<void>((resolve) => {
208
- wake = resolve
209
- })
210
- }
211
- }
212
- return {
213
- push: (message: SDKUserMessage) => {
214
- queue.push(message)
215
- poke()
216
- },
217
- close: () => {
218
- closed = true
219
- poke()
220
- },
221
- iterable: iterate(),
222
- }
223
- }
224
-
225
- const realClaudePath = (): string => {
226
- const found = Bun.which('claude')
227
- if (found === null) throw new Error('No `claude` binary on PATH.')
228
- return realpathSync(found)
229
- }
230
-
231
- const commonGitDir = async ({ cwd }: { cwd: string }): Promise<string> => {
232
- const result = await runCommand({
233
- cmd: ['git', 'rev-parse', '--path-format=absolute', '--git-common-dir'],
234
- cwd,
235
- timeout_ms: 30_000,
236
- })
237
- if (result.exit_code !== 0) {
238
- throw new Error(`git rev-parse failed in ${cwd}: ${result.stderr}`)
239
- }
240
- return realpathSync(result.stdout.trim())
241
- }
242
-
243
- /**
244
- * One agent session the launcher keeps open between turns, so a follow-up
245
- * reaches the same agent. Its guard hook, sandbox, and options are the ones
246
- * it was launched with.
247
- */
248
- type OpenSession = {
249
- role: AgentRole
250
- ticket: number
251
- cwd: string
252
- may_edit_tests: boolean
253
- running: AgentQuerySession
254
- input: ReturnType<typeof createInputChannel>
255
- /** What the launcher has seen of the session; per-turn fields reset each turn. */
256
- summary: AgentSession
257
- /** The SDK's running tokens per model, as of the session's last result. */
258
- model_totals: Record<string, ModelTokens>
259
- /** Resolves the turn waiting for the session's next outcome. */
260
- waiter: ((outcome: Outcome) => void) | null
261
- /** A stop or an engine failure: the session is over, with this outcome. */
262
- ended: Outcome | null
263
- /** Reads the session's messages until it ends. */
264
- pump: Promise<void>
265
- idle_timer: ReturnType<typeof setTimeout> | undefined
266
- stderr: string[]
267
- }
268
-
269
- /** The session's next outcome: a result, a stop, or its end. */
270
- const nextOutcome = (open: OpenSession): Promise<Outcome> =>
271
- open.ended !== null
272
- ? Promise.resolve(open.ended)
273
- : new Promise<Outcome>((resolve) => {
274
- open.waiter = resolve
275
- })
276
-
277
- const deliver = ({
278
- open,
279
- outcome,
280
- }: {
281
- open: OpenSession
282
- outcome: Outcome
283
- }): void => {
284
- if (!outcome.ok && outcome.failure !== 'agent') open.ended = outcome
285
- const waiter = open.waiter
286
- open.waiter = null
287
- waiter?.(outcome)
288
- }
289
-
290
- const stderrTail = (open: OpenSession): string =>
291
- open.stderr.length === 0 ? '' : `\n${open.stderr.join('').slice(-2000)}`
292
-
293
- /** A result's `modelUsage`, in the journal's words. */
294
- const modelTotalsOf = (
295
- modelUsage: z.infer<typeof ResultSchema>['modelUsage']
296
- ): Record<string, ModelTokens> =>
297
- Object.fromEntries(
298
- Object.entries(modelUsage).map(([name, each]) => [
299
- name,
300
- {
301
- input_tokens: each.inputTokens,
302
- output_tokens: each.outputTokens,
303
- cache_read_input_tokens: each.cacheReadInputTokens,
304
- cache_creation_input_tokens: each.cacheCreationInputTokens,
305
- },
306
- ])
307
- )
308
-
309
- /**
310
- * One turn's tokens per model: the session's running totals less those at
311
- * its last result, as the SDK sums `modelUsage` over a session's turns.
312
- */
313
- const turnTokens = ({
314
- totals,
315
- before,
316
- }: {
317
- totals: Record<string, ModelTokens>
318
- before: Record<string, ModelTokens>
319
- }): Record<string, ModelTokens> =>
320
- Object.fromEntries(
321
- Object.entries(totals).map(([name, now]) => {
322
- const was = before[name]
323
- const less = (key: keyof ModelTokens) =>
324
- Math.max(0, now[key] - (was?.[key] ?? 0))
325
- return [
326
- name,
327
- {
328
- input_tokens: less('input_tokens'),
329
- output_tokens: less('output_tokens'),
330
- cache_read_input_tokens: less('cache_read_input_tokens'),
331
- cache_creation_input_tokens: less(
332
- 'cache_creation_input_tokens'
333
- ),
334
- },
335
- ]
336
- })
337
- )
338
-
339
- /** Reads one message into the session's summary; an outcome ends the turn. */
340
- const watch = ({
341
- open,
342
- raw,
343
- }: {
344
- open: OpenSession
345
- raw: unknown
346
- }): Outcome | null => {
347
- const { summary } = open
348
- const init = InitSchema.safeParse(raw)
349
- if (init.success) {
350
- summary.session_id = init.data.session_id
351
- summary.model = init.data.model
352
- summary.claude_code_version = init.data.claude_code_version
353
- summary.api_key_source = init.data.apiKeySource
354
- const problem = initProblem(init.data)
355
- return problem === null ? null : stop(problem)
356
- }
357
- const rateLimit = RateLimitSchema.safeParse(raw)
358
- if (rateLimit.success) {
359
- const info = rateLimit.data.rate_limit_info
360
- summary.rate_limit_events.push({
361
- ...info,
362
- arrived_at: new Date().toISOString(),
363
- })
364
- const signal = planSignal({ info })
365
- switch (signal.kind) {
366
- case 'ok':
367
- return null
368
- case 'billing':
369
- return planCut(signal.reason)
370
- case 'limit':
371
- return planCut(
372
- `rate_limit_event status=rejected (${signal.rate_limit_type ?? 'unknown'})`
373
- )
374
- }
375
- }
376
- const assistant = AssistantSchema.safeParse(raw)
377
- if (assistant.success) {
378
- if (assistant.data.error !== 'billing_error') return null
379
- summary.billing_error = true
380
- return planCut('assistant error billing_error')
381
- }
382
- const result = ResultSchema.safeParse(raw)
383
- if (!result.success) return null
384
- const done = result.data
385
- summary.num_turns = done.num_turns
386
- summary.usage = done.usage
387
- const totals = modelTotalsOf(done.modelUsage)
388
- summary.model_usage = turnTokens({
389
- totals,
390
- before: open.model_totals,
391
- })
392
- open.model_totals = { ...open.model_totals, ...totals }
393
- summary.total_cost_usd = done.total_cost_usd
394
- summary.permission_denials = done.permission_denials.map(
395
- ({ tool_name, tool_use_id }) => ({ tool_name, tool_use_id })
396
- )
397
- if (done.subtype === 'success' && !done.is_error) {
398
- return { ok: true, structured_output: done.structured_output }
399
- }
400
- const detail = done.errors.length > 0 ? done.errors.join('; ') : done.result
401
- return {
402
- ok: false,
403
- failure: 'agent',
404
- error: `The agent's turn ended with ${done.subtype}: ${detail}`,
405
- }
406
- }
407
-
408
- /**
409
- * Reads a session's messages until it ends, handing each outcome to the turn
410
- * waiting for it, then calls `on_end`.
411
- */
412
- const pumpMessages = async ({
413
- open,
414
- on_end,
415
- }: {
416
- open: OpenSession
417
- on_end: () => void
418
- }): Promise<void> => {
419
- try {
420
- for await (const raw of open.running) {
421
- const outcome = watch({ open, raw })
422
- if (outcome === null) continue
423
- deliver({ open, outcome })
424
- if (open.ended !== null) break
425
- }
426
- if (open.ended !== null) {
427
- on_end()
428
- return
429
- }
430
- deliver({
431
- open,
432
- outcome: {
433
- ok: false,
434
- failure: 'engine',
435
- error: `The agent session ended with no result.${stderrTail(open)}`,
436
- },
437
- })
438
- } catch (error) {
439
- deliver({
440
- open,
441
- outcome: {
442
- ok: false,
443
- failure: 'engine',
444
- error: `The agent session failed: ${errorText(error)}${stderrTail(open)}`,
445
- },
446
- })
447
- }
448
- on_end()
449
- }
450
-
451
23
  /**
452
24
  * The real agent launcher: one Claude Agent SDK session per agent, with
453
25
  * every guard on. Per launch it:
454
26
  *
455
- * 1. refuses a Fable or non-Claude model before starting anything;
27
+ * 1. refuses any model but `opus`, `sonnet` or `haiku` before starting
28
+ * anything; follow-ups stay on their session's model;
456
29
  * 2. builds the role's locked-down options (`agentOptions`) with the guard
457
30
  * hook (honoring `may_edit_tests`), the sandbox, and a clean environment;
458
31
  * with `messaging` (test-writers and implementers), the `luca` server's
@@ -465,7 +38,10 @@ const pumpMessages = async ({
465
38
  * cuts the turn off as `plan`, with the reason in its session, for the
466
39
  * decision step to turn into a limit wait or a billing stop;
467
40
  * 5. returns the result's structured output and the session's id (from its
468
- * init message).
41
+ * init message);
42
+ * 6. tells the turn's `on_progress`, if given, what the agent is doing as it
43
+ * happens: its tokens so far, its latest action, the tool call still
44
+ * open, and its plan-usage readings (`turn-progress.ts`, #628).
469
45
  *
470
46
  * The session stays open after its turn, in streaming-input mode, so
471
47
  * `followUp` can send it another message; the same watch applies to every
@@ -479,21 +55,18 @@ const pumpMessages = async ({
479
55
  *
480
56
  * @example
481
57
  * const launcher = createClaudeLauncher({})
482
- * const turn = await launcher.launch({ role: 'implementer', ticket: 11, prompt, cwd, may_edit_tests: false, config })
58
+ * const turn = await launcher.launch({ role: 'implementer', ticket: 11, prompt, cwd, may_edit_tests: false, config, messaging, model: 'opus' })
483
59
  * // ... later, in a fix loop:
484
60
  * if (turn.ok) await launcher.followUp({ session_id: turn.session_id, role: 'implementer', ticket: 11, message, cwd, config })
485
61
  * await launcher.closeAll()
486
62
  */
487
63
  export const createClaudeLauncher = ({
488
- model,
489
64
  effort,
490
65
  query,
491
66
  claude_path,
492
67
  turn_timeout_ms,
493
68
  idle_timeout_ms,
494
69
  }: {
495
- /** Defaults to `CLAUDE_MODEL`, Claude Opus 5.5. */
496
- model?: string
497
70
  /** Defaults to `AGENT_EFFORT`, the same for every role. */
498
71
  effort?: EffortLevel
499
72
  query?: AgentQuery
@@ -503,241 +76,29 @@ export const createClaudeLauncher = ({
503
76
  /** Defaults to `DEFAULT_IDLE_TIMEOUT_MS`. */
504
77
  idle_timeout_ms?: number
505
78
  }): AgentLauncher & { closeAll: () => Promise<void> } => {
506
- const useModel = model ?? CLAUDE_MODEL
507
79
  const useEffort = effort ?? AGENT_EFFORT
508
80
  const startQuery: AgentQuery = query ?? sdkQuery
509
81
  const timeoutMs = turn_timeout_ms ?? DEFAULT_TURN_TIMEOUT_MS
510
82
  const idleMs = idle_timeout_ms ?? DEFAULT_IDLE_TIMEOUT_MS
511
- const sessions = new Map<string, OpenSession>()
512
-
513
- const close = async (open: OpenSession): Promise<void> => {
514
- clearTimeout(open.idle_timer)
515
- for (const [id, each] of sessions) {
516
- if (each === open) sessions.delete(id)
517
- }
518
- open.input.close()
519
- open.running.close()
520
- await Promise.race([open.pump, Bun.sleep(5_000)])
521
- }
522
-
523
- /** Keeps a session open for follow-ups until it sits idle too long. */
524
- const keepOpen = (open: OpenSession, session_id: string): void => {
525
- sessions.set(session_id, open)
526
- clearTimeout(open.idle_timer)
527
- open.idle_timer = setTimeout(() => void close(open), idleMs)
528
- open.idle_timer.unref?.()
529
- }
530
-
531
- /** Resets the summary's per-turn fields; the session's own stay. */
532
- const startTurn = (open: OpenSession): void => {
533
- clearTimeout(open.idle_timer)
534
- const fresh = AgentSessionSchema.parse({})
535
- open.summary = {
536
- ...fresh,
537
- session_id: open.summary.session_id,
538
- model: open.summary.model,
539
- claude_code_version: open.summary.claude_code_version,
540
- api_key_source: open.summary.api_key_source,
541
- subscription_type: open.summary.subscription_type,
542
- }
543
- }
544
-
545
- /**
546
- * Sends one message and waits for the turn's outcome. A success or an
547
- * agent failure keeps the session open for follow-ups; a stop, an
548
- * engine failure, or a timeout closes it.
549
- */
550
- const runTurn = async ({
551
- open,
552
- message,
553
- started,
554
- fallback_id,
555
- }: {
556
- open: OpenSession
557
- message: string
558
- started: number
559
- /** The id to report if the session never named itself. */
560
- fallback_id: string
561
- }): Promise<AgentTurn> => {
562
- const waiting = nextOutcome(open)
563
- open.input.push({
564
- type: 'user',
565
- message: { role: 'user', content: message },
566
- parent_tool_use_id: null,
567
- })
568
- let timer: ReturnType<typeof setTimeout> | undefined
569
- const timedOut = new Promise<'timeout'>((resolve) => {
570
- timer = setTimeout(() => resolve('timeout'), timeoutMs)
571
- })
572
- const raced = await Promise.race([waiting, timedOut])
573
- clearTimeout(timer)
574
- const outcome: Outcome =
575
- raced === 'timeout'
576
- ? {
577
- ok: false,
578
- failure: 'agent',
579
- error: `No result within ${timeoutMs} ms; the agent was interrupted.`,
580
- }
581
- : raced
582
- if (raced === 'timeout') {
583
- await open.running.interrupt().catch(() => undefined)
584
- }
585
- const session_id = open.summary.session_id ?? fallback_id
586
- const session = {
587
- ...open.summary,
588
- duration_ms: Date.now() - started,
589
- }
590
- const keep =
591
- raced !== 'timeout' &&
592
- open.ended === null &&
593
- open.summary.session_id !== null
594
- if (keep) keepOpen(open, session_id)
595
- else await close(open)
596
- return { ...outcome, session_id, session }
597
- }
598
-
599
- const launch: AgentLauncher['launch'] = async ({
600
- role,
601
- ticket,
602
- prompt,
603
- cwd,
604
- may_edit_tests,
605
- config,
606
- messaging,
607
- }) => {
608
- const refused = checkModel({ model: useModel })
609
- if (refused !== null) return stop(refused)
610
- const started = Date.now()
611
- const fallback_id = `claude-launch-${randomUUID()}`
612
-
613
- let claudePath: string
614
- try {
615
- claudePath = claude_path ?? realClaudePath()
616
- } catch (error) {
617
- return stop(errorText(error))
618
- }
619
- let worktree: string
620
- let gitDir: string
621
- try {
622
- worktree = realpathSync(cwd)
623
- gitDir = await commonGitDir({ cwd: worktree })
624
- } catch (error) {
625
- return { ok: false, failure: 'engine', error: errorText(error) }
626
- }
627
-
628
- const summary: AgentSession = AgentSessionSchema.parse({})
629
- const stderr: string[] = []
630
- const options = agentOptions({
631
- role,
632
- may_edit_tests,
633
- cwd: worktree,
634
- model: useModel,
635
- effort: useEffort,
636
- claude_path: claudePath,
637
- config,
638
- common_git_dir: gitDir,
639
- home: realpathSync(homedir()),
640
- env: agentEnv({ source: process.env }),
641
- guard_hook: createGuardHook({
642
- role: guardRoleOf({ role }),
643
- may_edit_tests,
644
- worktree,
645
- config,
646
- // The summary is swapped each turn, so this reads it late.
647
- on_deny: (denial) => open.summary.guard_denials.push(denial),
648
- }),
649
- // Kept in the session's options, so its follow-ups have them too.
650
- luca_server:
651
- messaging === null ? null : createLucaServer({ messaging }),
652
- delivery_hook:
653
- messaging === null ? null : createDeliveryHook({ messaging }),
654
- stderr: (data) => {
655
- stderr.push(data)
656
- if (stderr.length > 50) stderr.shift()
657
- },
658
- })
659
-
660
- const input = createInputChannel()
661
- let running: AgentQuerySession
662
- try {
663
- running = startQuery({ prompt: input.iterable, options })
664
- } catch (error) {
665
- return {
666
- ok: false,
667
- failure: 'engine',
668
- error: `The SDK did not start: ${errorText(error)}`,
669
- session: { ...summary, duration_ms: Date.now() - started },
670
- }
671
- }
672
- const open: OpenSession = {
673
- role,
674
- ticket,
675
- cwd,
676
- may_edit_tests,
677
- running,
678
- input,
679
- summary,
680
- model_totals: {},
681
- waiter: null,
682
- ended: null,
683
- pump: Promise.resolve(),
684
- idle_timer: undefined,
685
- stderr,
686
- }
687
- open.pump = pumpMessages({ open, on_end: () => void close(open) })
688
-
689
- const ended = async (outcome: Outcome): Promise<AgentTurn> => {
690
- await close(open)
691
- return {
692
- ...outcome,
693
- session_id: open.summary.session_id ?? fallback_id,
694
- session: { ...open.summary, duration_ms: Date.now() - started },
695
- }
696
- }
697
-
698
- let timer: ReturnType<typeof setTimeout> | undefined
699
- const timedOut = new Promise<'timeout'>((resolve) => {
700
- timer = setTimeout(() => resolve('timeout'), timeoutMs)
701
- })
702
- const account = await Promise.race([
703
- running.accountInfo().then(
704
- (info) => ({ info }),
705
- (error: unknown) => ({ error })
706
- ),
707
- nextOutcome(open).then((outcome) => ({ outcome })),
708
- timedOut,
709
- ])
710
- clearTimeout(timer)
711
- open.waiter = null
712
- if (account === 'timeout') {
713
- return ended({
714
- ok: false,
715
- failure: 'engine',
716
- error: 'accountInfo() did not answer in time.',
717
- })
718
- }
719
- if ('outcome' in account) return ended(account.outcome)
720
- if ('error' in account) {
721
- return ended({
722
- ok: false,
723
- failure: 'engine',
724
- error: `accountInfo() failed: ${errorText(account.error)}`,
725
- })
726
- }
727
- const accountParsed = AccountSchema.safeParse(account.info)
728
- summary.subscription_type = accountParsed.success
729
- ? (accountParsed.data.subscriptionType ?? null)
730
- : null
731
- const problem = accountProblem(account.info)
732
- if (problem !== null) return ended(stop(`credential check: ${problem}`))
733
- return runTurn({ open, message: prompt, started, fallback_id })
734
- }
83
+ const pool = createSessionPool({
84
+ turn_timeout_ms: timeoutMs,
85
+ idle_timeout_ms: idleMs,
86
+ })
87
+ const { sessions, close, startTurn, runTurn, closeAll } = pool
88
+ const launch = createLaunch({
89
+ effort: useEffort,
90
+ start_query: startQuery,
91
+ claude_path,
92
+ turn_timeout_ms: timeoutMs,
93
+ pool,
94
+ })
735
95
 
736
96
  const followUp: AgentLauncher['followUp'] = async ({
737
97
  session_id,
738
98
  role,
739
99
  ticket,
740
100
  message,
101
+ on_progress,
741
102
  }) => {
742
103
  const open = sessions.get(session_id)
743
104
  if (open === undefined || open.ended !== null) {
@@ -746,6 +107,7 @@ export const createClaudeLauncher = ({
746
107
  ok: false,
747
108
  failure: 'engine',
748
109
  session_id,
110
+ session_gone: true,
749
111
  error: `No open agent session ${session_id}: it closed or never started.`,
750
112
  }
751
113
  }
@@ -758,6 +120,7 @@ export const createClaudeLauncher = ({
758
120
  }
759
121
  }
760
122
  startTurn(open)
123
+ open.progress.start(on_progress)
761
124
  return runTurn({
762
125
  open,
763
126
  message,
@@ -773,9 +136,5 @@ export const createClaudeLauncher = ({
773
136
  if (open !== undefined) await close(open)
774
137
  }
775
138
 
776
- const closeAll = async (): Promise<void> => {
777
- await Promise.all([...new Set(sessions.values())].map(close))
778
- }
779
-
780
139
  return { launch, followUp, closeSession, closeAll }
781
140
  }