@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
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Fixtures shared by the run budget decision tests (#435, #543): a run with
3
+ * a budget in its engine config, agent turns that count their tokens per
4
+ * model, and the run stuck on its budget with the spec issue told.
5
+ */
6
+ import {
7
+ intakePassed,
8
+ practiceTicket,
9
+ runBranchCreated,
10
+ SESSIONS,
11
+ ticketBuilt,
12
+ withInstalls,
13
+ } from './build-fixtures'
14
+ import { recordsFrom } from './intake-fixtures'
15
+
16
+ import type { AgentRole } from '../agents/role-results'
17
+ import { decideSteps } from '../core/decide'
18
+ import type { TicketSnapshot } from '../intake/intake-schemas'
19
+ import type { JournalEntry } from '../journal/journal-record'
20
+
21
+ export const SUM = practiceTicket({ number: 11, title: 'Add sum' })
22
+ export const PRODUCT = practiceTicket({ number: 12, title: 'Add product' })
23
+
24
+ /** The budget the tests give the run in its engine config. */
25
+ export const BUDGET = 50_000
26
+
27
+ type Tokens = {
28
+ input_tokens: number
29
+ output_tokens: number
30
+ cache_read_input_tokens: number
31
+ cache_creation_input_tokens: number
32
+ }
33
+
34
+ export const tokens = (
35
+ input: number,
36
+ output: number,
37
+ cache_read: number,
38
+ cache_creation: number
39
+ ): Tokens => ({
40
+ input_tokens: input,
41
+ output_tokens: output,
42
+ cache_read_input_tokens: cache_read,
43
+ cache_creation_input_tokens: cache_creation,
44
+ })
45
+
46
+ export const OPUS = 'claude-opus-5-5'
47
+ export const HAIKU = 'claude-haiku-4-5-20251001'
48
+
49
+ /** One agent turn's session, with its tokens per model. */
50
+ export const turnSession = ({
51
+ ticket,
52
+ role,
53
+ model_usage,
54
+ }: {
55
+ ticket: number | null
56
+ role: AgentRole
57
+ model_usage: Record<string, Tokens>
58
+ }): JournalEntry => ({
59
+ kind: 'agent_session',
60
+ ticket,
61
+ role,
62
+ content: {
63
+ role,
64
+ session: {
65
+ session_id: SESSIONS[role],
66
+ usage: tokens(0, 0, 0, 0),
67
+ model_usage,
68
+ rate_limit_events: [],
69
+ billing_error: false,
70
+ },
71
+ },
72
+ })
73
+
74
+ /** A turn that counts exactly `counted` tokens, all output, on Opus. */
75
+ export const turnOf = ({
76
+ ticket,
77
+ role,
78
+ counted,
79
+ }: {
80
+ ticket: number
81
+ role: AgentRole
82
+ counted: number
83
+ }): JournalEntry =>
84
+ turnSession({
85
+ ticket,
86
+ role,
87
+ model_usage: { [OPUS]: tokens(0, counted, 0, 0) },
88
+ })
89
+
90
+ /**
91
+ * The decision step's actions for a run with these tickets and entries
92
+ * after intake. `budget` is the config's `run_budget_tokens`; leave it out
93
+ * for a config without one.
94
+ */
95
+ export const stepsAfter = ({
96
+ tickets,
97
+ entries,
98
+ budget,
99
+ }: {
100
+ tickets: TicketSnapshot[]
101
+ entries: JournalEntry[]
102
+ budget?: number
103
+ }) =>
104
+ decideSteps({
105
+ records: recordsFrom({
106
+ entries: [
107
+ ...intakePassed({ tickets }).map(
108
+ (entry): JournalEntry =>
109
+ entry.kind === 'run_started' && budget !== undefined
110
+ ? {
111
+ ...entry,
112
+ content: {
113
+ ...entry.content,
114
+ config: {
115
+ ...entry.content.config,
116
+ run_budget_tokens: budget,
117
+ },
118
+ },
119
+ }
120
+ : entry
121
+ ),
122
+ ...withInstalls({ entries }),
123
+ ],
124
+ }),
125
+ })
126
+
127
+ export const oneSteps = (entries: JournalEntry[], budget: number | undefined) =>
128
+ stepsAfter({ tickets: [SUM], entries, budget })
129
+
130
+ /** #11's worktree, baseline, and tests written: its red check is next. */
131
+ export const sumWritten = (): JournalEntry[] =>
132
+ ticketBuilt({ ticket: 11 }).slice(0, 3)
133
+
134
+ /** #11 with its tests written, and its test-writer's turn counting `counted`. */
135
+ export const sumSpent = (counted: number): JournalEntry[] => [
136
+ runBranchCreated(),
137
+ ...sumWritten(),
138
+ turnOf({ ticket: 11, role: 'test-writer', counted }),
139
+ ]
140
+
141
+ /** The run went stuck on its budget. */
142
+ export const runStuck = (): JournalEntry => ({
143
+ kind: 'run_stuck',
144
+ ticket: null,
145
+ role: null,
146
+ content: {
147
+ reason: 'run_budget',
148
+ detail: `The run used ${BUDGET} tokens of its ${BUDGET} budget.`,
149
+ },
150
+ })
151
+
152
+ /** The engine told the spec issue the run is stuck, in comment 150. */
153
+ export const runStuckReported = (): JournalEntry => ({
154
+ kind: 'stuck_reported',
155
+ ticket: null,
156
+ role: null,
157
+ content: { comment_id: 150, body: 'stuck' },
158
+ })
159
+
160
+ /** #11 used the whole budget; the run is stuck and the spec issue told. */
161
+ export const told = (): JournalEntry[] => [
162
+ ...sumSpent(BUDGET),
163
+ runStuck(),
164
+ runStuckReported(),
165
+ ]
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Journal entry builders for a run's scaffolding: its config and tickets, its
3
+ * branch and worktrees, the installs and baseline tests, and the end of the
4
+ * run (worktrees removed, the PR opened).
5
+ */
6
+ import { SPEC_OWNER } from './intake-fixtures'
7
+
8
+ import type { EngineConfig } from '../config/engine-config'
9
+ import type { TestRun } from '../gates/gate-schemas'
10
+ import type { TicketSnapshot } from '../intake/intake-schemas'
11
+ import type { JournalEntry } from '../journal/journal-record'
12
+
13
+ export const BUILD_CONFIG: EngineConfig = {
14
+ checks: { test: 'bun test', types: 'bun run types', lint: 'bun run lint' },
15
+ test_file_patterns: ['**/*.test.ts'],
16
+ test_setup_files: [],
17
+ rule_files: [],
18
+ }
19
+
20
+ /** A ticket as intake snapshots it. */
21
+ export const practiceTicket = ({
22
+ number,
23
+ title,
24
+ criteria,
25
+ labels,
26
+ blockers,
27
+ }: {
28
+ number: number
29
+ title?: string
30
+ criteria?: string[]
31
+ /** Defaults to `ready-for-agent` only; add `refactor` for a refactor ticket. */
32
+ labels?: string[]
33
+ /** The tickets it waits on. Defaults to none. */
34
+ blockers?: number[]
35
+ }): TicketSnapshot => ({
36
+ number,
37
+ title: title ?? 'Add sum',
38
+ body: '## What to build\n\nA sum function.',
39
+ labels: labels ?? ['ready-for-agent'],
40
+ url: `https://github.com/acme/app/issues/${number}`,
41
+ criteria: (criteria ?? ['sum adds two numbers']).map((text, index) => ({
42
+ id: `AC${index + 1}`,
43
+ text,
44
+ })),
45
+ blockers: blockers ?? [],
46
+ })
47
+
48
+ /** A run whose intake passed with these tickets, in this order. */
49
+ export const intakePassed = ({
50
+ tickets,
51
+ }: {
52
+ tickets: TicketSnapshot[]
53
+ }): JournalEntry[] => [
54
+ {
55
+ kind: 'run_started',
56
+ ticket: null,
57
+ role: null,
58
+ content: { spec_number: 10, config: BUILD_CONFIG, base_branch: 'main' },
59
+ },
60
+ {
61
+ kind: 'spec_snapshot',
62
+ ticket: 10,
63
+ role: null,
64
+ content: {
65
+ spec: {
66
+ number: 10,
67
+ title: 'Practice spec',
68
+ body: '## Testing Decisions\n\n- Test the sum.',
69
+ labels: ['ready-for-agent'],
70
+ url: 'https://github.com/acme/app/issues/10',
71
+ author: SPEC_OWNER,
72
+ },
73
+ ticket_order: tickets.map((ticket) => ticket.number),
74
+ closed_tickets: [],
75
+ },
76
+ },
77
+ ...tickets.map(
78
+ (ticket): JournalEntry => ({
79
+ kind: 'ticket_snapshot',
80
+ ticket: ticket.number,
81
+ role: null,
82
+ content: ticket,
83
+ })
84
+ ),
85
+ ]
86
+
87
+ export const RUN_BRANCH = 'luca/spec-10-run'
88
+
89
+ /** Where fixtures put each ticket's worktree, and the run branch's. */
90
+ export const ticketPath = (ticket: number): string =>
91
+ `/runs/run/tickets/${ticket}`
92
+
93
+ export const RUN_BRANCH_PATH = '/runs/run/run-branch'
94
+
95
+ export const runBranchCreated = (): JournalEntry => ({
96
+ kind: 'run_branch_created',
97
+ ticket: null,
98
+ role: null,
99
+ content: {
100
+ branch: RUN_BRANCH,
101
+ path: RUN_BRANCH_PATH,
102
+ base_sha: 'b0',
103
+ },
104
+ })
105
+
106
+ export const ticketWorktreeCreated = ({
107
+ ticket,
108
+ }: {
109
+ ticket: number
110
+ }): JournalEntry => ({
111
+ kind: 'ticket_worktree_created',
112
+ ticket,
113
+ role: null,
114
+ content: {
115
+ branch: `${RUN_BRANCH}--ticket-${ticket}`,
116
+ path: ticketPath(ticket),
117
+ base_sha: 'b0',
118
+ },
119
+ })
120
+
121
+ /**
122
+ * The engine's install in a new worktree: the run branch's checkout
123
+ * (`ticket` unset) or a ticket's worktree. Passing unless `ok` is false.
124
+ */
125
+ export const dependenciesInstalled = ({
126
+ ticket,
127
+ ok,
128
+ }: {
129
+ ticket?: number
130
+ ok?: boolean
131
+ }): JournalEntry => ({
132
+ kind: 'dependencies_installed',
133
+ ticket: ticket ?? null,
134
+ role: null,
135
+ content: {
136
+ target: ticket === undefined ? 'run_branch' : 'ticket',
137
+ check: {
138
+ name: 'install',
139
+ command: 'bun install --frozen-lockfile',
140
+ ok: ok ?? true,
141
+ exit_code: ok === false ? 1 : 0,
142
+ output:
143
+ ok === false
144
+ ? 'error: lockfile had changes, but lockfile is frozen'
145
+ : '',
146
+ },
147
+ },
148
+ })
149
+
150
+ /**
151
+ * The entries with a passing install after each new worktree that has none,
152
+ * so tests about later steps need not list the installs.
153
+ */
154
+ export const withInstalls = ({
155
+ entries,
156
+ }: {
157
+ entries: JournalEntry[]
158
+ }): JournalEntry[] =>
159
+ entries.flatMap((entry, index) => {
160
+ const made =
161
+ entry.kind === 'run_branch_created' ||
162
+ entry.kind === 'ticket_worktree_created'
163
+ if (!made || entries[index + 1]?.kind === 'dependencies_installed') {
164
+ return [entry]
165
+ }
166
+ return [
167
+ entry,
168
+ dependenciesInstalled({ ticket: entry.ticket ?? undefined }),
169
+ ]
170
+ })
171
+
172
+ /** A test run with no test files, as a fresh repo has. */
173
+ export const emptyTestRun = (): TestRun => ({
174
+ command: 'bun test',
175
+ ok: true,
176
+ exit_code: 1,
177
+ no_test_files: true,
178
+ cases: [],
179
+ files_without_results: [],
180
+ output: '0 test files matching',
181
+ })
182
+
183
+ export const baselineTests = ({
184
+ ticket,
185
+ }: {
186
+ ticket: number
187
+ }): JournalEntry => ({
188
+ kind: 'baseline_tests',
189
+ ticket,
190
+ role: null,
191
+ content: emptyTestRun(),
192
+ })
193
+
194
+ /** The engine removed these worktrees at the end of the run. */
195
+ export const worktreesRemoved = ({
196
+ paths,
197
+ }: {
198
+ paths: string[]
199
+ }): JournalEntry => ({
200
+ kind: 'worktrees_removed',
201
+ ticket: null,
202
+ role: null,
203
+ content: { paths },
204
+ })
205
+
206
+ /** The run's one PR, #99, opened from the run branch. */
207
+ export const pullRequestOpened = (): JournalEntry => ({
208
+ kind: 'pull_request_opened',
209
+ ticket: null,
210
+ role: null,
211
+ content: {
212
+ number: 99,
213
+ url: 'https://github.com/acme/app/pull/99',
214
+ head: RUN_BRANCH,
215
+ base: 'main',
216
+ title: 'Practice spec (#10)',
217
+ body: '',
218
+ },
219
+ })
@@ -0,0 +1,177 @@
1
+ import { realpathSync } from 'node:fs'
2
+
3
+ import type { HookJSONOutput, Options } from '@anthropic-ai/claude-agent-sdk'
4
+
5
+ import type { AgentQuery } from '../agents/claude-launcher'
6
+ import { fakeQuery, repo } from '../agents/claude-launcher-helpers'
7
+
8
+ /**
9
+ * A fake SDK session for the launcher's tests: SDK-shaped messages played
10
+ * through `createClaudeLauncher`'s `query`, and tool calls played through
11
+ * the guard hooks the launch set up, as the SDK would. No model, no process.
12
+ */
13
+
14
+ /** A step: an SDK message, or work to do once the launcher has read the message before it. */
15
+ export type Step = unknown | ((options: Options) => Promise<void>)
16
+
17
+ /**
18
+ * A fake `query` that plays `steps`: each message is yielded in turn, and
19
+ * each function runs after the launcher has read the message before it,
20
+ * the way the agent's tool calls happen mid-turn.
21
+ */
22
+ export const playing = ({ steps }: { steps: Step[] }): AgentQuery => {
23
+ const messages: unknown[] = []
24
+ const after = new Map<unknown, ((options: Options) => Promise<void>)[]>()
25
+ for (const step of steps) {
26
+ if (typeof step === 'function') {
27
+ const last = messages.at(-1)
28
+ after.set(last, [
29
+ ...(after.get(last) ?? []),
30
+ step as (options: Options) => Promise<void>,
31
+ ])
32
+ } else messages.push(step)
33
+ }
34
+ const fake = fakeQuery({ messages })
35
+ return (params) => {
36
+ const inner = fake.query(params)
37
+ return {
38
+ ...inner,
39
+ [Symbol.asyncIterator]: async function* () {
40
+ for await (const message of inner) {
41
+ yield message
42
+ for (const work of after.get(message) ?? []) {
43
+ await work(params.options)
44
+ }
45
+ }
46
+ },
47
+ }
48
+ }
49
+ }
50
+
51
+ /** One message's token counts, as the SDK reports them. */
52
+ export type Usage = {
53
+ input_tokens: number
54
+ output_tokens: number
55
+ cache_read_input_tokens: number
56
+ cache_creation_input_tokens: number
57
+ }
58
+
59
+ /**
60
+ * One API response, as the SDK streams it: the agent's (`parent` null), or
61
+ * a helper's (`parent` the `Agent` call that started it).
62
+ */
63
+ export const assistant = ({
64
+ id,
65
+ content,
66
+ parent = null,
67
+ model = 'claude-opus-5-5',
68
+ usage = {
69
+ input_tokens: 10,
70
+ output_tokens: 10,
71
+ cache_read_input_tokens: 0,
72
+ cache_creation_input_tokens: 0,
73
+ },
74
+ }: {
75
+ id: string
76
+ content: unknown[]
77
+ parent?: string | null
78
+ model?: string
79
+ usage?: Usage
80
+ }) => ({
81
+ type: 'assistant',
82
+ message: {
83
+ id,
84
+ type: 'message',
85
+ role: 'assistant',
86
+ model,
87
+ content,
88
+ stop_reason: 'tool_use',
89
+ usage,
90
+ },
91
+ parent_tool_use_id: parent,
92
+ session_id: 'session-1',
93
+ uuid: `uuid-${id}`,
94
+ })
95
+
96
+ /** A tool call inside an `assistant` message's content. */
97
+ export const toolUse = (
98
+ id: string,
99
+ name: string,
100
+ input: Record<string, unknown>
101
+ ) => ({ type: 'tool_use', id, name, input })
102
+
103
+ /** A tool's result, handed back to the agent (`parent` null) or a helper. */
104
+ export const toolResult = ({
105
+ tool_use_id,
106
+ content,
107
+ parent = null,
108
+ }: {
109
+ tool_use_id: string
110
+ content: string
111
+ parent?: string | null
112
+ }) => ({
113
+ type: 'user',
114
+ message: {
115
+ role: 'user',
116
+ content: [{ type: 'tool_result', tool_use_id, content }],
117
+ },
118
+ parent_tool_use_id: parent,
119
+ session_id: 'session-1',
120
+ })
121
+
122
+ /**
123
+ * Plays one tool call through every `PreToolUse` hook the launch set up
124
+ * whose matcher fits the tool, as the SDK would, and gives the first
125
+ * denial, or `{}` when every hook let it through. With `helper`, the call
126
+ * comes from inside a helper (the SDK's `agent_id` and `agent_type`). `cwd`
127
+ * is the test's repo, its real path, unless told.
128
+ */
129
+ export const preToolUse = async ({
130
+ options,
131
+ tool_name,
132
+ tool_input,
133
+ tool_use_id,
134
+ helper = false,
135
+ cwd = realpathSync(repo),
136
+ }: {
137
+ options: Options
138
+ tool_name: string
139
+ tool_input: Record<string, unknown>
140
+ tool_use_id: string
141
+ helper?: boolean
142
+ cwd?: string
143
+ }): Promise<HookJSONOutput> => {
144
+ const signal = new AbortController().signal
145
+ for (const matcher of options.hooks?.PreToolUse ?? []) {
146
+ if (
147
+ matcher.matcher !== undefined &&
148
+ !new RegExp(`^(?:${matcher.matcher})$`).test(tool_name)
149
+ )
150
+ continue
151
+ for (const hook of matcher.hooks) {
152
+ const answer = await hook(
153
+ {
154
+ hook_event_name: 'PreToolUse',
155
+ tool_name,
156
+ tool_input,
157
+ tool_use_id,
158
+ session_id: 'session-1',
159
+ transcript_path: '/dev/null',
160
+ cwd,
161
+ ...(helper
162
+ ? { agent_id: 'helper-1', agent_type: 'searcher' }
163
+ : {}),
164
+ },
165
+ tool_use_id,
166
+ { signal }
167
+ )
168
+ if (
169
+ 'hookSpecificOutput' in answer &&
170
+ answer.hookSpecificOutput?.hookEventName === 'PreToolUse' &&
171
+ answer.hookSpecificOutput.permissionDecision === 'deny'
172
+ )
173
+ return answer
174
+ }
175
+ }
176
+ return {}
177
+ }