@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,283 @@
1
+ /**
2
+ * A turn's live progress (#628), for the engine's live file: what the
3
+ * agent and its helpers (#636) are doing now, read from the SDK messages (streamed chunks too,
4
+ * #631) and the guard hook. It
5
+ * holds only short words: a tool and a target (a path relative to the
6
+ * worktree, or the first words of a command), never a prompt, a file's
7
+ * contents or a command's output.
8
+ */
9
+ import { basename, isAbsolute, join, relative } from 'node:path'
10
+
11
+ import { z } from 'zod'
12
+
13
+ import { RateLimitSchema, toolResultIds } from './claude-messages'
14
+
15
+ import { HELPER_MODEL, HELPER_TOOL } from '../guards/helper-rules'
16
+
17
+ /** The agent's latest action: a tool and a short target, with its time (ISO). */
18
+ export type ProgressAction = { tool: string; target: string; at: string }
19
+
20
+ /** A tool call the guard let through whose result hasn't come back yet. */
21
+ export type ProgressOpenCall = {
22
+ tool: string
23
+ target: string
24
+ started_at: string
25
+ }
26
+
27
+ /**
28
+ * A helper the agent started this turn (#636): its model, its latest
29
+ * action, and whether its result has come back.
30
+ */
31
+ export type ProgressHelper = {
32
+ model: string
33
+ action: ProgressAction | null
34
+ done: boolean
35
+ }
36
+
37
+ /** What one turn has done so far, as `on_progress` hears it. */
38
+ export type TurnProgress = {
39
+ /** Every token the turn's messages read or wrote so far, cache reads included. */
40
+ tokens: number
41
+ action: ProgressAction | null
42
+ open_call: ProgressOpenCall | null
43
+ /** The turn's helpers, oldest first; none when left out. */
44
+ helpers?: ProgressHelper[]
45
+ /** The turn's `rate_limit_event` readings so far, each with `arrived_at`. */
46
+ rate_limit_events: Record<string, unknown>[]
47
+ }
48
+
49
+ /** Hears a turn's progress each time it changes. */
50
+ export type ProgressListener = (progress: TurnProgress) => void
51
+
52
+ /** How many words of a command a target keeps. */
53
+ const COMMAND_WORDS = 3
54
+
55
+ /** The longest target, in characters. */
56
+ const MAX_TARGET = 60
57
+
58
+ const UsageSchema = z.object({
59
+ input_tokens: z.number().default(0),
60
+ output_tokens: z.number().default(0),
61
+ cache_read_input_tokens: z.number().nullable().default(0),
62
+ cache_creation_input_tokens: z.number().nullable().default(0),
63
+ })
64
+
65
+ const AssistantSchema = z.object({
66
+ type: z.literal('assistant'),
67
+ uuid: z.string().optional(),
68
+ parent_tool_use_id: z.string().nullable().default(null),
69
+ message: z.object({
70
+ id: z.string().optional(),
71
+ content: z.array(z.unknown()).default([]),
72
+ usage: UsageSchema.optional(),
73
+ }),
74
+ })
75
+
76
+ const ToolUseSchema = z.object({
77
+ type: z.literal('tool_use'),
78
+ name: z.string(),
79
+ input: z.unknown(),
80
+ })
81
+
82
+ /** The parts of a tool call's input a target is made from. */
83
+ const TargetInputSchema = z
84
+ .object({
85
+ file_path: z.unknown(),
86
+ notebook_path: z.unknown(),
87
+ command: z.unknown(),
88
+ pattern: z.unknown(),
89
+ subagent_type: z.unknown(),
90
+ })
91
+ .partial()
92
+
93
+ /** A streamed chunk of the model's answer (#631), with partial messages on. */
94
+ const StreamEventSchema = z.object({ type: z.literal('stream_event') })
95
+
96
+ const textOf = (value: unknown): string | null =>
97
+ typeof value === 'string' && value.trim() !== '' ? value.trim() : null
98
+
99
+ /** A path relative to the worktree; one outside it keeps its name only. */
100
+ const shortPath = ({
101
+ path,
102
+ worktree,
103
+ }: {
104
+ path: string
105
+ worktree: string
106
+ }): string => {
107
+ const inside = relative(
108
+ worktree,
109
+ isAbsolute(path) ? path : join(worktree, path)
110
+ )
111
+ return inside.startsWith('..') || isAbsolute(inside)
112
+ ? basename(path)
113
+ : inside
114
+ }
115
+
116
+ /** The first words of a command's first line. */
117
+ const firstWords = (command: string): string =>
118
+ (command.split('\n')[0] ?? '')
119
+ .trim()
120
+ .match(new RegExp(`^\\S+(?:\\s+\\S+){0,${COMMAND_WORDS - 1}}`))?.[0] ??
121
+ ''
122
+
123
+ /**
124
+ * A tool call's short target: the path it works on, the first words of
125
+ * its command, its search pattern, or its helper's type.
126
+ *
127
+ * @example
128
+ * targetOf({ tool_name: 'Bash', tool_input: { command: 'bun test src --bail' }, worktree }) // 'bun test src'
129
+ */
130
+ export const targetOf = ({
131
+ tool_name,
132
+ tool_input,
133
+ worktree,
134
+ }: {
135
+ tool_name: string
136
+ tool_input: unknown
137
+ worktree: string
138
+ }): string => {
139
+ const parsed = TargetInputSchema.safeParse(tool_input)
140
+ const input = parsed.success ? parsed.data : {}
141
+ const path = textOf(input.file_path) ?? textOf(input.notebook_path)
142
+ const target =
143
+ path !== null
144
+ ? shortPath({ path, worktree })
145
+ : tool_name === 'Bash'
146
+ ? firstWords(textOf(input.command) ?? '')
147
+ : (textOf(input.pattern) ?? textOf(input.subagent_type) ?? '')
148
+ return target.slice(0, MAX_TARGET)
149
+ }
150
+
151
+ /**
152
+ * Keeps one session's turn progress and tells `on_progress` each time it
153
+ * changes. `start` begins a turn afresh, with its own listener;
154
+ * `readings` gives the turn's plan-usage readings, as the session's
155
+ * summary has them.
156
+ *
157
+ * @example
158
+ * const progress = createTurnProgress({ worktree, readings: () => summary.rate_limit_events })
159
+ * progress.start(on_progress)
160
+ * progress.heard(message)
161
+ */
162
+ export const createTurnProgress = ({
163
+ worktree,
164
+ readings,
165
+ }: {
166
+ worktree: string
167
+ readings: () => Record<string, unknown>[]
168
+ }) => {
169
+ let listener: ProgressListener | undefined
170
+ let tokens = new Map<string, number>()
171
+ let action: ProgressAction | null = null
172
+ let open = new Map<string, ProgressOpenCall>()
173
+ /** The turn's helpers, by the `Agent` call that started each. */
174
+ let helpers = new Map<string, ProgressHelper>()
175
+
176
+ const tell = (): void => {
177
+ listener?.({
178
+ tokens: [...tokens.values()].reduce((sum, each) => sum + each, 0),
179
+ action,
180
+ open_call: [...open.values()].at(-1) ?? null,
181
+ helpers: [...helpers.values()].map((helper) => ({ ...helper })),
182
+ rate_limit_events: [...readings()],
183
+ })
184
+ }
185
+
186
+ const start = (on_progress: ProgressListener | undefined): void => {
187
+ listener = on_progress
188
+ tokens = new Map()
189
+ action = null
190
+ open = new Map()
191
+ helpers = new Map()
192
+ }
193
+
194
+ /**
195
+ * Reads one SDK message: its tokens, its tool calls, the results back. A
196
+ * streamed chunk is told as it comes, so the heartbeat moves while the
197
+ * model thinks, but nothing of it is kept: the whole message that
198
+ * follows carries the tokens.
199
+ */
200
+ const heard = (raw: unknown): void => {
201
+ if (
202
+ RateLimitSchema.safeParse(raw).success ||
203
+ StreamEventSchema.safeParse(raw).success
204
+ ) {
205
+ tell()
206
+ return
207
+ }
208
+ const assistant = AssistantSchema.safeParse(raw)
209
+ if (assistant.success) {
210
+ const { message, uuid, parent_tool_use_id } = assistant.data
211
+ const usage = message.usage
212
+ if (usage !== undefined) {
213
+ tokens.set(
214
+ message.id ?? uuid ?? String(tokens.size),
215
+ usage.input_tokens +
216
+ usage.output_tokens +
217
+ (usage.cache_read_input_tokens ?? 0) +
218
+ (usage.cache_creation_input_tokens ?? 0)
219
+ )
220
+ }
221
+ // A helper's own calls are its action, not its agent's.
222
+ const helper =
223
+ parent_tool_use_id === null
224
+ ? undefined
225
+ : helpers.get(parent_tool_use_id)
226
+ for (const block of message.content) {
227
+ const call = ToolUseSchema.safeParse(block)
228
+ if (!call.success) continue
229
+ const latest = {
230
+ tool: call.data.name,
231
+ target: targetOf({
232
+ tool_name: call.data.name,
233
+ tool_input: call.data.input,
234
+ worktree,
235
+ }),
236
+ at: new Date().toISOString(),
237
+ }
238
+ if (parent_tool_use_id === null) action = latest
239
+ else if (helper !== undefined) helper.action = latest
240
+ }
241
+ tell()
242
+ return
243
+ }
244
+ const ids = toolResultIds(raw)
245
+ if (!ids.some((id) => open.has(id) || helpers.has(id))) return
246
+ for (const id of ids) {
247
+ open.delete(id)
248
+ const helper = helpers.get(id)
249
+ if (helper !== undefined) helper.done = true
250
+ }
251
+ tell()
252
+ }
253
+
254
+ /** A tool call the guard let through: open until its result comes back. */
255
+ const opened = ({
256
+ tool_name,
257
+ tool_input,
258
+ tool_use_id,
259
+ }: {
260
+ tool_name: string
261
+ tool_input: unknown
262
+ tool_use_id: string
263
+ }): void => {
264
+ open.set(tool_use_id, {
265
+ tool: tool_name,
266
+ target: targetOf({ tool_name, tool_input, worktree }),
267
+ started_at: new Date().toISOString(),
268
+ })
269
+ if (tool_name === HELPER_TOOL) {
270
+ helpers.set(tool_use_id, {
271
+ model: HELPER_MODEL,
272
+ action: null,
273
+ done: false,
274
+ })
275
+ }
276
+ tell()
277
+ }
278
+
279
+ return { start, heard, opened }
280
+ }
281
+
282
+ /** The progress `createTurnProgress` keeps for one session. */
283
+ export type TurnProgressKeeper = ReturnType<typeof createTurnProgress>
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The task sections of the roles that write: the test-writer and the
3
+ * implementer.
4
+ */
5
+ import { engineChores, REVIEW_FIXER, setupFiles } from './common-instructions'
6
+
7
+ import {
8
+ caseLimitOf,
9
+ featureMapPath,
10
+ isFeatureMapFolder,
11
+ slowTestCommands,
12
+ testCommands,
13
+ type EngineConfig,
14
+ } from '../config/engine-config'
15
+ import { MAX_TESTS_PER_CRITERION } from '../core/loop-caps'
16
+
17
+ const TEST_UPDATE = `## Updating tests after a rebase
18
+ Sometimes other tickets joined the run branch after this ticket's tests were written, and the engine moved this ticket's change on top of it. If the implementer then says one of your tests is wrong, the tests come back to you. Your prompt says what joined, and the test and the implementer's reason. Then:
19
+ - The ticket's code is already in the worktree. Never touch it; edit test files only.
20
+ - Change a test only where the run branch's behavior changed what it should expect. Read what joined with \`git log\` and \`git show\`. Don't weaken a test just to make it pass.
21
+ - Every criterion still needs a test. Your updated tests may pass now, since the code is there; that is fine. The engine runs every test in the gates after the implementer's turn.
22
+ - Answer "tests_written" with the full criterion mapping again.`
23
+
24
+ /**
25
+ * When a writer may answer `environment` (#613), and that the engine checks
26
+ * the claim by rerunning the failed check.
27
+ */
28
+ const environmentLine = ({ check }: { check: string }): string =>
29
+ `- If ${check} failed for a reason that is not this ticket's fault (the machine or another ticket: a port already in use, EADDRINUSE; an old test that also fails at baseline), don't fake a fix: answer outcome "environment", with your evidence in "environment": { evidence } (the error lines, and what you checked). The engine checks the claim by rerunning the failed check once, with no fix round used. If the rerun fails the same way, the ticket is stuck for the owner, so answer it only when you are sure.`
30
+
31
+ /**
32
+ * The test-writer's lines about the feature map (#583), or nothing without
33
+ * one. A folder map (#597) is read page by page, and a new test file goes on
34
+ * its area's page.
35
+ */
36
+ const featureMapLines = ({ config }: { config: EngineConfig }): string => {
37
+ const path = featureMapPath({ config })
38
+ if (path === null) return ''
39
+ return isFeatureMapFolder({ path })
40
+ ? `- Read the feature map's pages (the *.md files under ${path}/) first: they list each feature and the test files that prove it.\n- Put new tests in a feature's existing test files when you can. A new test file must be listed under its feature on its area's page in ${path}/; you may edit or add those pages.\n`
41
+ : `- Read ${path} first: it lists each feature and the test files that prove it.\n- Put new tests in a feature's existing test files when you can. A new test file must be listed under its feature in ${path}; you may edit that file.\n`
42
+ }
43
+
44
+ /** What the test-writer's "ONLY test files" line adds for the feature map, or nothing. */
45
+ const featureMapClause = ({ config }: { config: EngineConfig }): string => {
46
+ const path = featureMapPath({ config })
47
+ if (path === null) return ''
48
+ return isFeatureMapFolder({ path })
49
+ ? ` and the feature map's pages (*.md under ${path}/)`
50
+ : ` and the feature map ${path}`
51
+ }
52
+
53
+ /** What the test-writer's "ONLY test files" line adds for test helpers (#619), or nothing. */
54
+ const testHelperClause = ({ config }: { config: EngineConfig }): string => {
55
+ const patterns = config.test_helper_patterns ?? []
56
+ return patterns.length === 0
57
+ ? ''
58
+ : `, test helpers only tests use, such as fakes and fixtures (${patterns.join(', ')})`
59
+ }
60
+
61
+ export const TEST_WRITER = ({
62
+ config,
63
+ }: {
64
+ config: EngineConfig
65
+ }) => `# Your role: test-writer
66
+
67
+ You write the failing tests for ONE ticket, before any code exists.
68
+
69
+ - Create or edit ONLY test files (${config.test_file_patterns.join(', ')})${testHelperClause({ config })}${featureMapClause({ config })}. No other file: no implementation, no package.json, no config, no notes, and not the test setup files.
70
+ - Your new tests must FAIL right now, because the code does not exist yet. That is expected. Do not create or stub the module under test.
71
+ - Test only the public behavior the ticket names, at the seams the spec's "Testing Decisions" section names. No mocks unless the spec asks. No tautological asserts.
72
+ ${featureMapLines({ config })}- Every acceptance criterion needs at least one test, and at most ${MAX_TESTS_PER_CRITERION}: write the fewest tests that prove it. Old tests must keep passing.
73
+ - Map every test you add. The red check refuses a new test your criterion mapping doesn't name.
74
+ - Give every test a plain string-literal name (no test.each, no template names), so the engine can find each one before the module exists.
75
+
76
+ Your result (structured output):
77
+ - outcome: "tests_written", or "nothing_new_to_test" if the ticket truly changes no behavior (a refactor), or "already_done" if the ticket's work is already on the base branch, or "needs_setup_change" if the tests can't work without a change to a test setup file${setupFiles({ config })} (you may never change one): say which file and why in "setup_change", and the user makes the change.
78
+ - Answer "already_done" only when EVERY acceptance criterion is already met on the base branch and already has a test that checks it. Look with \`git log\` and \`git show\`, and read the code and tests. Then write no tests, and give your evidence: in "done_by", the commits that did the work, as { sha, title }; in "criteria", the existing tests that cover each criterion. The engine checks this evidence: each commit must be on the base branch, and each test must exist and pass there. If even one criterion is not met, or not tested, write the tests instead.
79
+ - criteria: for EACH criterion id (AC1, AC2, ...), the tests that check it, as { file, name }. "name" is the full name bun prints: describe names and the test name joined by " > ".
80
+ ${environmentLine({ check: 'the red check' })}
81
+ - done_by: empty, unless the outcome is "already_done".
82
+ - finding_responses: empty, unless your prompt gives you ticket review findings (see below).
83
+ - file_modes, run_generators: empty, unless you need a chore below.
84
+ - summary, assumptions, run_notes.
85
+
86
+ ${engineChores({ config })}
87
+
88
+ ${TEST_UPDATE}
89
+
90
+ ${REVIEW_FIXER}`
91
+
92
+ const testFileRule = ({
93
+ config,
94
+ may_edit_tests,
95
+ }: {
96
+ config: EngineConfig
97
+ may_edit_tests: boolean
98
+ }): string =>
99
+ may_edit_tests
100
+ ? `- You may follow renames and moves into test files (${config.test_file_patterns.join(', ')}), but never change what a test checks. Never touch a test setup file${setupFiles({ config })}.`
101
+ : `- Never create, edit, or delete a test file (${config.test_file_patterns.join(', ')}) or a test setup file${setupFiles({ config })}.`
102
+
103
+ /** The implementer's line about the slow tests (#563), or nothing without them. */
104
+ const slowLine = ({ config }: { config: EngineConfig }): string => {
105
+ const slow = slowTestCommands({ config }).map(({ run }) => `\`${run}\``)
106
+ return slow.length === 0
107
+ ? ''
108
+ : ` The slow tests (${slow.join(', ')}) run at the final review, and the ticket's gates also run the slow test files it added or changed; you may run them yourself.`
109
+ }
110
+
111
+ /**
112
+ * The implementer's line about the fast test limit (#580), or nothing without
113
+ * one.
114
+ */
115
+ const limitLine = ({ config }: { config: EngineConfig }): string => {
116
+ const limit = caseLimitOf({ config })
117
+ return limit === null
118
+ ? ''
119
+ : `- Each fast test must finish within ${limit} ms. If one of the ticket's tests can't, answer "bad_test".\n`
120
+ }
121
+
122
+ export const IMPLEMENTER = ({
123
+ config,
124
+ may_edit_tests,
125
+ }: {
126
+ config: EngineConfig
127
+ may_edit_tests: boolean
128
+ }) => `# Your role: implementer
129
+
130
+ You write the code that makes ONE ticket's failing tests pass. Another agent wrote the tests; the engine already committed them.
131
+
132
+ - Make every gate pass: ${[
133
+ ...testCommands({ config }).map(({ run }) => run),
134
+ config.checks.types,
135
+ config.checks.lint,
136
+ ]
137
+ .filter((command) => command !== undefined)
138
+ .map((command) => `\`${command}\``)
139
+ .join(', ')}.${slowLine({ config })}
140
+ ${limitLine({ config })}${testFileRule({ config, may_edit_tests })}
141
+ - If a test is wrong (it contradicts the ticket or spec, or no correct code can pass it), don't work around it: answer outcome "bad_test" with the test and your reason.
142
+ - If the gates can't pass without a change to a test setup file${setupFiles({ config })}, don't work around it: answer outcome "needs_setup_change", with the file and why in "setup_change". The user makes the change.
143
+ ${environmentLine({ check: 'a gate' })}
144
+ - Follow the spec's Implementation Decisions. Keep the change small: no extra features, and no new dependencies unless the spec says so.
145
+ - Leave nothing behind: no scratch files, logs, notes, or unused scripts. The engine scans for leftovers before it commits.
146
+
147
+ Your result (structured output): outcome ("done", "bad_test", "needs_setup_change", or "environment"), bad_test (null unless outcome is "bad_test"), setup_change (null unless outcome is "needs_setup_change"), environment (null unless outcome is "environment"), finding_responses (empty unless you were sent ticket review findings), file_modes and run_generators (empty unless you need a chore below), summary, assumptions, run_notes.
148
+
149
+ ${engineChores({ config })}
150
+
151
+ ${REVIEW_FIXER}`
@@ -372,12 +372,24 @@ export const boardDir = async (): Promise<string> => {
372
372
  }
373
373
 
374
374
  /**
375
- * The installed Luca: its version (see `lucaVersion`), its board folder,
376
- * the real path of its `luca-run`, and Bun's own path.
375
+ * The skills folder in Luca's install folder: `skills/` next to `engine/`
376
+ * in the published package, or `packages/engine/skills` in a working copy
377
+ * (both two folders up from this file). Its real path when it exists;
378
+ * `luca doctor` reports it when it doesn't.
379
+ */
380
+ export const skillsDir = async (): Promise<string> => {
381
+ const dir = join(import.meta.dir, '..', '..', 'skills')
382
+ return realpath(dir).catch(() => dir)
383
+ }
384
+
385
+ /**
386
+ * The installed Luca: its version (see `lucaVersion`), its board and skills
387
+ * folders, the real path of its `luca-run`, and Bun's own path.
377
388
  */
378
389
  export const lucaInstall = async (): Promise<LucaInstall> => ({
379
390
  luca_version: lucaVersion(),
380
391
  board_dir: await boardDir(),
392
+ skills_dir: await skillsDir(),
381
393
  engine_path: await realpath(join(import.meta.dir, 'luca-run.ts')),
382
394
  bun_path: await realpath(process.execPath),
383
395
  })
@@ -388,7 +400,7 @@ const PackageJsonSchema = z.looseObject({
388
400
  })
389
401
 
390
402
  /** The package a file belongs to: the nearest package.json with a name. */
391
- const packageOf = async (
403
+ export const packageOf = async (
392
404
  file: string
393
405
  ): Promise<{ name: string; version: string | null } | null> => {
394
406
  for (let dir = dirname(file); ; dir = dirname(dir)) {
@@ -115,6 +115,8 @@ export type LucaInstall = {
115
115
  luca_version: string
116
116
  /** The board folder inside Luca's install folder. */
117
117
  board_dir: string
118
+ /** The skills folder inside Luca's install folder (Luca's own skills). */
119
+ skills_dir: string
118
120
  /** The real path of the installed `luca-run`. */
119
121
  engine_path: string
120
122
  /** Bun's own path. */
@@ -22,17 +22,24 @@ import {
22
22
  type DoctorCheck,
23
23
  type Found,
24
24
  } from './doctor-checks'
25
+ import { claudeSkillsDir, skillDrift } from './luca-skills'
25
26
  import { isRight, rightEntry, userMuninnEntry } from './muninn-entry'
26
27
 
27
28
  import { LUCA_PACKAGE } from '../config/luca-version'
29
+ import {
30
+ loadDecisionModelCredentials,
31
+ missingKeyLines,
32
+ type Env,
33
+ } from '../jev/decision-model-credentials'
34
+ import { DEFAULT_DECISION_MODEL } from '../jev/jev-schemas'
28
35
  import { READY_LABEL } from '../tracker/tracker'
29
36
 
30
37
  /**
31
38
  * `luca doctor`'s checks of this computer, read-only: Bun, the `luca`
32
39
  * copies on the PATH, Claude Code, the `gh` login, Paseo and its plugins,
33
- * the board, MuninnDB and Claude Code's `muninn` entry, and the planning
34
- * skills. `luca init` and `luca upgrade` end with them too. The token is
35
- * never shown.
40
+ * the board, MuninnDB and Claude Code's `muninn` entry, the planning
41
+ * skills, Luca's own skills, and the decision model's credentials. `luca
42
+ * init` and `luca upgrade` end with them too. No token is ever shown.
36
43
  */
37
44
 
38
45
  /** The oldest tool versions Luca works with. */
@@ -327,17 +334,77 @@ const checkPlanningSkills = async ({ home }: { home: string }) => {
327
334
  return ok('The planning skills write what intake needs')
328
335
  }
329
336
 
337
+ /**
338
+ * Luca's own skills (#504): each one in the installed Luca's skills folder
339
+ * (#529) is in `~/.claude/skills`, and its files are the ones it ships.
340
+ */
341
+ const checkLucaSkills = async ({
342
+ home,
343
+ skills_dir,
344
+ luca_version,
345
+ }: {
346
+ home: string
347
+ skills_dir: string
348
+ luca_version: string
349
+ }) => {
350
+ const drift = await skillDrift({ home, skills_dir })
351
+ if (drift.length === 0) {
352
+ return problem({
353
+ detail: `Luca's folder ${skills_dir} has no skills.`,
354
+ fix: `Reinstall Luca: bun add -g ${LUCA_PACKAGE}@alpha`,
355
+ })
356
+ }
357
+ const off = drift.filter(
358
+ ({ missing, changed }) => missing.length + changed.length > 0
359
+ )
360
+ if (off.length > 0) {
361
+ const described = off.map(({ skill, missing }) =>
362
+ missing.includes('SKILL.md')
363
+ ? `/${skill} isn't installed`
364
+ : `/${skill} isn't the one Luca ${luca_version} ships`
365
+ )
366
+ return problem({
367
+ detail: `In ${claudeSkillsDir({ home })}, ${described.join('; ')}.`,
368
+ fix: 'Run luca doctor --fix (or luca init) to install it.',
369
+ })
370
+ }
371
+ return ok(
372
+ `Luca's skills (${drift.map(({ skill }) => `/${skill}`).join(', ')}) match Luca ${luca_version}`
373
+ )
374
+ }
375
+
376
+ /**
377
+ * The decision model's Cloudflare credentials (#534), from Luca's own env
378
+ * file and the process env. With none it is off: a warning, not a problem,
379
+ * since runs go on without it. Never shows the token.
380
+ */
381
+ const checkDecisionModel = async ({
382
+ env,
383
+ home,
384
+ }: {
385
+ env: Env
386
+ home: string
387
+ }) => {
388
+ const credentials = await loadDecisionModelCredentials({ env, home })
389
+ if (credentials.ok) return ok(`Clef set up (${DEFAULT_DECISION_MODEL})`)
390
+ return warning({
391
+ detail: `Decision model off: no credentials (${credentials.missing.join(' and ')} not set in ${credentials.file}).`,
392
+ fix: `Add these lines to ${credentials.file}: ${missingKeyLines(credentials).join(', ')} (a Workers AI token from the Cloudflare dashboard), or leave the decision model off.`,
393
+ })
394
+ }
395
+
330
396
  /**
331
397
  * Runs the computer checks, in order, read-only. Never throws: a check that
332
398
  * fails becomes a problem.
333
399
  *
334
400
  * @example
335
- * const checks = await computerChecks({ home, luca_version, board_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo })
401
+ * const checks = await computerChecks({ home, luca_version, board_dir, skills_dir, engine_path, bun_path, computer, muninn, muninn_health, claude, paseo, env: process.env })
336
402
  */
337
403
  export const computerChecks = async ({
338
404
  home,
339
405
  luca_version,
340
406
  board_dir,
407
+ skills_dir,
341
408
  engine_path,
342
409
  bun_path,
343
410
  computer,
@@ -345,11 +412,14 @@ export const computerChecks = async ({
345
412
  muninn_health,
346
413
  claude,
347
414
  paseo,
415
+ env,
348
416
  }: {
349
417
  home: string
350
418
  /** The installed Luca's version. */
351
419
  luca_version: string
352
420
  board_dir: string
421
+ /** The skills folder inside Luca's install folder. */
422
+ skills_dir: string
353
423
  engine_path: string
354
424
  bun_path: string
355
425
  computer: Computer
@@ -357,6 +427,11 @@ export const computerChecks = async ({
357
427
  muninn_health: MuninnHealth
358
428
  claude: ClaudeMcp
359
429
  paseo: Paseo
430
+ /**
431
+ * The process env, for the decision model's keys and `XDG_CONFIG_HOME`.
432
+ * Left out, none: only Luca's env file in `home` counts.
433
+ */
434
+ env?: Env
360
435
  }): Promise<DoctorCheck[]> => {
361
436
  // With Paseo down, its plugins and the board aren't asked about.
362
437
  let paseo_up = true
@@ -399,6 +474,14 @@ export const computerChecks = async ({
399
474
  ['muninndb', () => checkMuninnDb({ muninn, muninn_health })],
400
475
  ['muninn_entry', () => checkMuninnEntry({ muninn, claude })],
401
476
  ['planning_skills', () => checkPlanningSkills({ home })],
477
+ [
478
+ 'luca_skills',
479
+ () => checkLucaSkills({ home, skills_dir, luca_version }),
480
+ ],
481
+ [
482
+ 'decision_model',
483
+ () => checkDecisionModel({ env: env ?? {}, home }),
484
+ ],
402
485
  ],
403
486
  })
404
487
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The log line for a resume's config reload (#516): what a `config_reloaded`
3
+ * changed, or why a `config_reload_refused` kept the run's own config.
4
+ */
5
+ import type { JournalRecord } from '../journal/journal-record'
6
+
7
+ /** A value of a reloaded config field in a log line; `null` is "none". */
8
+ const fieldValue = (value: string | number | string[] | null): string => {
9
+ if (value === null) return 'none'
10
+ if (Array.isArray(value)) return `[${value.join(', ')}]`
11
+ return String(value)
12
+ }
13
+
14
+ /**
15
+ * The log line for a resume's `config_reloaded` or `config_reload_refused`
16
+ * (#516).
17
+ *
18
+ * @example
19
+ * configReloadText({ reload }) // 'config reloaded: prepare changed (make rom -> make rom-cached)'
20
+ */
21
+ export const configReloadText = ({
22
+ reload,
23
+ }: {
24
+ reload: JournalRecord
25
+ }): string => {
26
+ switch (reload.kind) {
27
+ case 'config_reloaded':
28
+ return `config reloaded: ${reload.content.changes
29
+ .map(
30
+ ({ field, from, to }) =>
31
+ `${field} changed (${fieldValue(from)} -> ${fieldValue(to)})`
32
+ )
33
+ .join(', ')}`
34
+ case 'config_reload_refused':
35
+ return `kept the run's config, as the repo's can't be used: ${reload.content.reason}`
36
+ default:
37
+ return reload.kind
38
+ }
39
+ }