@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,236 @@
1
+ /**
2
+ * The run in the board's state: where it stands (status and phase), why it
3
+ * stopped, the run's own facts, the budget, the stuck work that needs a
4
+ * reply, the tickets left out for a person, and the run list's summaries.
5
+ */
6
+ import { z } from 'zod'
7
+
8
+ /**
9
+ * Why the run stopped and what to do, in words. A billing stop, or a stop
10
+ * after the same step crashed again and again, won't go on; any other stop
11
+ * picks up again when the run is started with its run id.
12
+ *
13
+ * @example
14
+ * stoppedText({ reason: 'overage in use', billing: true })
15
+ * // 'Stopped for billing: overage in use. This run will not go on; ...'
16
+ */
17
+ export const stoppedText = ({
18
+ reason,
19
+ billing,
20
+ crashed,
21
+ }: {
22
+ reason: string
23
+ billing: boolean
24
+ /** The same run-level step crashed again and again. */
25
+ crashed?: boolean
26
+ }): string => {
27
+ if (billing) {
28
+ return `Stopped for billing: ${reason}. This run will not go on; start a new run once per-token billing is off.`
29
+ }
30
+ if (crashed === true) {
31
+ return `Stopped after crashes: ${reason} This run will not go on; read the engine's log for why it crashed, then start a new run.`
32
+ }
33
+ return `The run stopped: ${reason}. Start it again with the same run id to pick up where it stopped.`
34
+ }
35
+
36
+ /** Where a run stands, as one word for the header row and the panel. */
37
+ export const RunStatusSchema = z.enum([
38
+ 'starting',
39
+ 'intake',
40
+ 'building',
41
+ 'final_review',
42
+ 'limit_wait',
43
+ 'usage_line_wait',
44
+ 'done',
45
+ 'refused',
46
+ 'nothing_to_do',
47
+ 'stuck',
48
+ 'stopped',
49
+ 'ended_with_error',
50
+ ])
51
+
52
+ export type RunStatus = z.infer<typeof RunStatusSchema>
53
+
54
+ /** The part of a run's status the journal sets directly. */
55
+ export const RunPhaseSchema = z.enum([
56
+ 'starting',
57
+ 'intake',
58
+ 'building',
59
+ 'final_review',
60
+ 'done',
61
+ 'refused',
62
+ 'nothing_to_do',
63
+ ])
64
+
65
+ export type RunPhase = z.infer<typeof RunPhaseSchema>
66
+
67
+ /** Stuck work waiting for a one-word reply on the spec issue. */
68
+ export const NeedsYouSchema = z.object({
69
+ /** `ticket-<n>`, `final`, or `run` (the run stuck on its budget). */
70
+ key: z.string(),
71
+ ticket: z.number().int().nullable(),
72
+ subject: z.string(),
73
+ reason: z.string(),
74
+ detail: z.string(),
75
+ /** What was tried before it got stuck, oldest first. */
76
+ tried: z.array(z.string()),
77
+ /** The exact replies to post as a comment on the spec issue. */
78
+ replies: z.array(z.string()),
79
+ since: z.string(),
80
+ })
81
+
82
+ export type NeedsYou = z.infer<typeof NeedsYouSchema>
83
+
84
+ /**
85
+ * The engine's default run budget in tokens (its
86
+ * `DEFAULT_RUN_BUDGET_TOKENS`, in `engine/src/limits/run-budget.ts`), for a
87
+ * run whose engine config sets no `run_budget_tokens`. The plugin never
88
+ * loads the engine's code, so the number is kept here too.
89
+ */
90
+ export const DEFAULT_RUN_BUDGET_TOKENS = 9_000_000
91
+
92
+ /**
93
+ * The engine's default `models.confidence_bar` (in
94
+ * `engine/src/config/engine-config-schema.ts`): the decision model is sharp at or
95
+ * over it, split under it. Kept here for the same reason.
96
+ */
97
+ export const DEFAULT_CONFIDENCE_BAR = 0.8
98
+
99
+ export const EngineEndedSchema = z.object({
100
+ ok: z.boolean(),
101
+ message: z.string(),
102
+ })
103
+
104
+ export type EngineEnded = z.infer<typeof EngineEndedSchema>
105
+
106
+ /**
107
+ * The run stopped because going on was unsafe (wrong credentials or plan,
108
+ * a rejected rate limit, overage, ...). Starting it again with the same run
109
+ * id picks the step up again, except after a billing stop, which won't go on.
110
+ */
111
+ export const RunStoppedSchema = z.object({
112
+ reason: z.string(),
113
+ role: z.string().nullable(),
114
+ ticket: z.number().int().nullable(),
115
+ /** The session would bill per token; this run won't go on. */
116
+ billing: z.boolean(),
117
+ /** The same step crashed again and again; this run won't go on. */
118
+ crashed: z.boolean().default(false),
119
+ since: z.string(),
120
+ })
121
+
122
+ export type RunStopped = z.infer<typeof RunStoppedSchema>
123
+
124
+ export const RunInfoSchema = z.object({
125
+ run_id: z.string(),
126
+ /** `null` for a demo run until the engine names its spec. */
127
+ spec_number: z.number().int().nullable(),
128
+ spec_title: z.string().nullable(),
129
+ demo: z.boolean(),
130
+ branch: z.string().nullable(),
131
+ phase: RunPhaseSchema,
132
+ status: RunStatusSchema,
133
+ pr_url: z.string().nullable(),
134
+ pr_number: z.number().int().nullable(),
135
+ /** Why intake refused the run, one line per problem. */
136
+ refusal: z.array(z.string()),
137
+ started_at: z.string(),
138
+ last_time: z.string().nullable(),
139
+ engine_ended: EngineEndedSchema.nullable(),
140
+ /** Set by `run_stopped`, cleared once the engine moves on. */
141
+ stopped: RunStoppedSchema.nullable(),
142
+ log_path: z.string().nullable(),
143
+ /**
144
+ * Luca's version the run started on (#460); `null` when its journal
145
+ * doesn't say. Defaulted, so an older board state still parses.
146
+ */
147
+ started_on: z.string().nullable().default(null),
148
+ /**
149
+ * Set once the run resumed on a different Luca version than it started
150
+ * on, naming both. The run keeps going. Defaulted, so an older board
151
+ * state still parses.
152
+ */
153
+ version_note: z.string().nullable().default(null),
154
+ /**
155
+ * The spec's owner (its issue's author), from intake (#503): only their
156
+ * replies count. `null` when the journal doesn't say. Defaulted, so an
157
+ * older board state still parses.
158
+ */
159
+ spec_author: z.string().nullable().default(null),
160
+ /** The spec's repo as `owner/repo`, from its URL; `null` when not known. */
161
+ spec_repo: z.string().nullable().default(null),
162
+ })
163
+
164
+ export type RunInfo = z.infer<typeof RunInfoSchema>
165
+
166
+ /** The run phases after which no engine is needed. */
167
+ export const OVER_PHASES: ReadonlySet<RunPhase> = new Set<RunPhase>([
168
+ 'done',
169
+ 'refused',
170
+ 'nothing_to_do',
171
+ ])
172
+
173
+ /**
174
+ * Whether a run may still need an engine: not ended, not over. The
175
+ * server's engine check and the panel's "no sign of life" both go by it.
176
+ *
177
+ * @example
178
+ * needsEngine({ state }) // true while the run builds
179
+ */
180
+ export const needsEngine = ({ state }: { state: { run: RunInfo } }): boolean =>
181
+ state.run.engine_ended === null && !OVER_PHASES.has(state.run.phase)
182
+
183
+ /**
184
+ * An open ticket the run left out for a person (#499): one labeled
185
+ * `ready-for-human`, or one waiting on such a ticket. Not a problem, so the
186
+ * board lists it calmly and nobody is asked to act.
187
+ */
188
+ export const PersonTicketSchema = z.object({
189
+ number: z.number().int(),
190
+ title: z.string(),
191
+ /** Why, in words: `for a person`, or `waits on #12, which is for a person`. */
192
+ why: z.string(),
193
+ })
194
+
195
+ export type PersonTicket = z.infer<typeof PersonTicketSchema>
196
+
197
+ /** One run in the panel's run list. */
198
+ export const RunSummarySchema = z.object({
199
+ run_id: z.string(),
200
+ spec_number: z.number().int().nullable(),
201
+ spec_title: z.string().nullable(),
202
+ demo: z.boolean(),
203
+ status: RunStatusSchema,
204
+ started_at: z.string(),
205
+ needs_you: z.number().int().min(0),
206
+ })
207
+
208
+ export type RunSummary = z.infer<typeof RunSummarySchema>
209
+
210
+ /** How many of the newest runs get a tab; the older ones are folded. */
211
+ export const RECENT_RUN_TABS = 6
212
+
213
+ /**
214
+ * Splits runs (newest first) into the ones with a tab and the folded older
215
+ * ones. A picked older run keeps a tab too, after the newest ones.
216
+ *
217
+ * @example
218
+ * const { shown, folded } = foldRuns({ runs, selected: picked })
219
+ */
220
+ export const foldRuns = ({
221
+ runs,
222
+ selected,
223
+ }: {
224
+ runs: RunSummary[]
225
+ selected: string | null
226
+ }): { shown: RunSummary[]; folded: RunSummary[] } => {
227
+ const shown = runs.slice(0, RECENT_RUN_TABS)
228
+ const older = runs.slice(RECENT_RUN_TABS)
229
+ const picked = older.find((run) => run.run_id === selected)
230
+ return picked
231
+ ? {
232
+ shown: [...shown, picked],
233
+ folded: older.filter((run) => run !== picked),
234
+ }
235
+ : { shown, folded: older }
236
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * A ticket's card in the board's state: its steps and stage, its findings,
3
+ * the check that is open, the step the engine is in, and the card itself.
4
+ */
5
+ import { z } from 'zod'
6
+
7
+ import { LiveAgentSchema, LiveWaitSchema } from './board-state-live'
8
+ import { PlanUsedSchema } from './board-state-usage'
9
+
10
+ /** The steps of a ticket, shown as dots on its card. */
11
+ export const STEP_NAMES = [
12
+ 'tests',
13
+ 'red check',
14
+ 'code',
15
+ 'checks',
16
+ 'review',
17
+ ] as const
18
+
19
+ /** Refactor tickets skip the first two steps: no new test can fail first. */
20
+ export const REFACTOR_SKIPS_STEPS = 2
21
+
22
+ /** The step index a ticket has when every step is done. */
23
+ export const ALL_STEPS_DONE = STEP_NAMES.length
24
+
25
+ /**
26
+ * What a ticket is really doing (#549): `building` its first test-writer and
27
+ * implementer pass, `reviewing` only while a reviewer works, `testing` the red
28
+ * check or the checks, `fixing` any fix round, `joining` the run branch and
29
+ * the checks there. Every older stage is still one, so a stored board state
30
+ * from before still parses.
31
+ */
32
+ export const TicketStageSchema = z.enum([
33
+ 'blocked',
34
+ 'building',
35
+ 'reviewing',
36
+ 'testing',
37
+ 'fixing',
38
+ 'joining',
39
+ 'stuck',
40
+ 'done',
41
+ 'skipped',
42
+ ])
43
+
44
+ export type TicketStage = z.infer<typeof TicketStageSchema>
45
+
46
+ export const FindingCountsSchema = z.object({
47
+ blocker: z.number().int().min(0),
48
+ should_fix: z.number().int().min(0),
49
+ nit: z.number().int().min(0),
50
+ })
51
+
52
+ export type FindingCounts = z.infer<typeof FindingCountsSchema>
53
+
54
+ /**
55
+ * A check whose failure went back to an agent: its fix loop is open. `review`
56
+ * is a ticket review that asked for changes: the next fixer starts a review
57
+ * fix round.
58
+ */
59
+ export const OpenCheckSchema = z.enum(['red_check', 'gates', 'review'])
60
+
61
+ export type OpenCheck = z.infer<typeof OpenCheckSchema>
62
+
63
+ /**
64
+ * What the engine is doing right now for a ticket or the run, from its
65
+ * `step_started`, such as "running the baseline tests"; `since` is that
66
+ * record's time. Cleared by the step's `step_ended`.
67
+ */
68
+ export const CurrentStepSchema = z.object({
69
+ text: z.string(),
70
+ since: z.string(),
71
+ })
72
+
73
+ export type CurrentStep = z.infer<typeof CurrentStepSchema>
74
+
75
+ /** The baseline tests' step (`run_baseline_tests`) in words. */
76
+ export const BASELINE_STEP_TEXT = 'running the baseline tests'
77
+
78
+ /**
79
+ * What a ticket waits for between steps (#559): `baseline` another
80
+ * ticket's shared baseline tests, `join` the join queue, `start` the start
81
+ * queue, each on `on_ticket`. From the engine's `ticket_waiting`.
82
+ */
83
+ export const WaitForSchema = z.enum(['baseline', 'join', 'start'])
84
+
85
+ export type WaitFor = z.infer<typeof WaitForSchema>
86
+
87
+ /**
88
+ * A ticket between steps, since `since`: from `ticket_waiting` with what it
89
+ * waits for, or from its last `step_ended` with `for` and `on_ticket` null
90
+ * (an old journal's wait, worked out from the board). Cleared by the
91
+ * ticket's next `step_started`.
92
+ */
93
+ export const TicketWaitSchema = z.object({
94
+ for: WaitForSchema.nullable(),
95
+ on_ticket: z.number().int().nullable(),
96
+ since: z.string(),
97
+ })
98
+
99
+ export type TicketWait = z.infer<typeof TicketWaitSchema>
100
+
101
+ /**
102
+ * A time span in words: seconds under a minute, then minutes and seconds,
103
+ * then hours and minutes.
104
+ *
105
+ * @example
106
+ * elapsedText({ ms: 125_000 }) // '2m 5s'
107
+ * elapsedText({ ms: 3_790_000 }) // '1h 3m'
108
+ */
109
+ export const elapsedText = ({ ms }: { ms: number }): string => {
110
+ const seconds = Math.max(0, Math.floor(ms / 1000))
111
+ const hours = Math.floor(seconds / 3600)
112
+ const minutes = Math.floor((seconds % 3600) / 60)
113
+ if (hours > 0) return `${hours}h ${minutes}m`
114
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`
115
+ return `${seconds}s`
116
+ }
117
+
118
+ /**
119
+ * The current step in words, with how long it has been running.
120
+ *
121
+ * @example
122
+ * currentStepText({
123
+ * step: { text: 'running the checks', since: '2026-09-23T12:00:00.000Z' },
124
+ * now: Date.parse('2026-09-23T12:02:05.000Z'),
125
+ * }) // 'running the checks (2m 5s)'
126
+ */
127
+ export const currentStepText = ({
128
+ step,
129
+ now,
130
+ }: {
131
+ step: CurrentStep
132
+ now: number
133
+ }): string =>
134
+ `${step.text} (${elapsedText({ ms: now - Date.parse(step.since) })})`
135
+
136
+ /**
137
+ * The decision model's answer to an agent's `agent_model` ask: the model it
138
+ * picked and how sure it was, either `null` when the answer lacks it.
139
+ */
140
+ export const ModelAnswerSchema = z.object({
141
+ role: z.string(),
142
+ pick: z.string().nullable(),
143
+ confidence: z.number().nullable(),
144
+ })
145
+
146
+ export type ModelAnswer = z.infer<typeof ModelAnswerSchema>
147
+
148
+ /**
149
+ * The model the card's latest agent runs on (#630), from its
150
+ * `agent_started`, and why: its `model_reason` (`null` in older journals)
151
+ * and its `agent_model` answer, if any.
152
+ */
153
+ export const AgentModelSchema = z.object({
154
+ role: z.string(),
155
+ name: z.string(),
156
+ reason: z.string().nullable(),
157
+ answer: ModelAnswerSchema.nullable(),
158
+ })
159
+
160
+ export type AgentModel = z.infer<typeof AgentModelSchema>
161
+
162
+ export const TicketCardSchema = z.object({
163
+ number: z.number().int(),
164
+ title: z.string(),
165
+ refactor: z.boolean(),
166
+ /** Tickets of this run this one waits on. */
167
+ blockers: z.array(z.number().int()),
168
+ /** The engine has begun work on it (its worktree exists). */
169
+ started: z.boolean(),
170
+ stage: TicketStageSchema,
171
+ /**
172
+ * Index into STEP_NAMES of the current step: -1 not started, 5 when every
173
+ * step is done. A stuck or skipped ticket keeps the step it stopped at.
174
+ */
175
+ step: z.number().int().min(-1).max(ALL_STEPS_DONE),
176
+ /** One-word (or two) activity, such as "coding" or "reviewing". */
177
+ activity: z.string(),
178
+ /** The role working on it now, `null` when no agent is. */
179
+ role: z.string().nullable(),
180
+ /**
181
+ * The latest agent's model and why; `null` before one starts, or when
182
+ * its journal names no model. Defaulted, so an older board state still
183
+ * parses.
184
+ */
185
+ model: AgentModelSchema.nullable().default(null),
186
+ /**
187
+ * The decision model's latest `agent_model` answer, kept until its
188
+ * agent starts. Defaulted, so an older board state still parses.
189
+ */
190
+ model_answer: ModelAnswerSchema.nullable().default(null),
191
+ /**
192
+ * Each role's last used `agent_model` answer, by role, for its
193
+ * follow-ups, which ask nothing new. Defaulted, so an older board state
194
+ * still parses.
195
+ */
196
+ model_answers: z.record(z.string(), ModelAnswerSchema).default({}),
197
+ /**
198
+ * The engine's step on it now, `null` between steps. Defaulted, so a
199
+ * board state from before it still parses.
200
+ */
201
+ current_step: CurrentStepSchema.nullable().default(null),
202
+ /**
203
+ * The ticket between steps, `null` in a step or before its first.
204
+ * Defaulted, so a board state from before it still parses.
205
+ */
206
+ wait: TicketWaitSchema.nullable().default(null),
207
+ /** Fix rounds in the current fix loop; back to 0 once its check passes. */
208
+ fix_round: z.number().int().min(0),
209
+ review_round: z.number().int().min(0),
210
+ /** Review fix rounds so far (up to LOOP_CAP): fixes after a review. */
211
+ review_fix_round: z.number().int().min(0),
212
+ /** The check whose failure the next follow-up answers, if any. */
213
+ open_check: OpenCheckSchema.nullable(),
214
+ /** The role whose latest turn failed, until an agent finishes again. */
215
+ failed_turn: z.string().nullable(),
216
+ tests: z
217
+ .object({
218
+ failing: z.number().int().min(0),
219
+ total: z.number().int().min(0),
220
+ })
221
+ .nullable(),
222
+ /**
223
+ * Its own baseline's test counts, kept so a ticket that reuses this one's
224
+ * baseline (`baseline_reused`) gets them after `tests` has moved on.
225
+ * Defaulted, so a board state from before it still parses.
226
+ */
227
+ baseline_tests: z
228
+ .object({
229
+ failing: z.number().int().min(0),
230
+ total: z.number().int().min(0),
231
+ })
232
+ .nullable()
233
+ .default(null),
234
+ findings: FindingCountsSchema.nullable(),
235
+ /** Every token the ticket's agents read or wrote, cache reads included. */
236
+ tokens: z.number().min(0),
237
+ /** The same tokens by role. */
238
+ agent_tokens: z.record(z.string(), z.number().min(0)),
239
+ /** What was tried on it so far (failed checks, fix rounds), oldest first. */
240
+ tried: z.array(z.string()),
241
+ /** How much of the plan the ticket used, per window; empty until known. */
242
+ plan_used: z.array(PlanUsedSchema),
243
+ /**
244
+ * Its agent's entry in the run's live file (#628), `null` with none.
245
+ * Laid over the state on each read, never journaled. Defaulted, so a
246
+ * board state from before it still parses.
247
+ */
248
+ live: LiveAgentSchema.nullable().default(null),
249
+ /**
250
+ * Its check's wait for a prepare slot or a test slot (#632), from the
251
+ * live file the same way, `null` with none. Defaulted, so a board
252
+ * state from before it still parses.
253
+ */
254
+ slot_wait: LiveWaitSchema.nullable().default(null),
255
+ })
256
+
257
+ export type TicketCard = z.infer<typeof TicketCardSchema>
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Plan usage in the board's state: the color level of a percent, plan
3
+ * windows in words and in their shown order, how much of a window something
4
+ * used, and the plan's latest reading.
5
+ */
6
+ import { z } from 'zod'
7
+
8
+ /** Plan usage is yellow from this percent. */
9
+ export const USAGE_WARN_FROM = 60
10
+
11
+ /** Plan usage is red above this percent. */
12
+ export const USAGE_HIGH_ABOVE = 85
13
+
14
+ export const UsageLevelSchema = z.enum(['ok', 'warn', 'high'])
15
+
16
+ export type UsageLevel = z.infer<typeof UsageLevelSchema>
17
+
18
+ /**
19
+ * The color level of a plan usage percent: `ok` (green) below 60, `warn`
20
+ * (yellow) from 60 to 85, `high` (red) above 85.
21
+ *
22
+ * @example
23
+ * usageLevel({ percent: 59 }) // 'ok'
24
+ * usageLevel({ percent: 85 }) // 'warn'
25
+ * usageLevel({ percent: 86 }) // 'high'
26
+ */
27
+ export const usageLevel = ({ percent }: { percent: number }): UsageLevel => {
28
+ if (percent > USAGE_HIGH_ABOVE) return 'high'
29
+ if (percent >= USAGE_WARN_FROM) return 'warn'
30
+ return 'ok'
31
+ }
32
+
33
+ /** Plan windows in words; any other window keeps its raw name. */
34
+ const WINDOW_WORDS: Record<string, string> = {
35
+ five_hour: 'five-hour',
36
+ seven_day: 'weekly',
37
+ seven_day_opus: 'weekly Opus',
38
+ seven_day_sonnet: 'weekly Sonnet',
39
+ }
40
+
41
+ /**
42
+ * A plan window's name in words.
43
+ *
44
+ * @example
45
+ * windowWords({ window: 'seven_day_opus' }) // 'weekly Opus'
46
+ * windowWords({ window: 'overage' }) // 'overage'
47
+ */
48
+ export const windowWords = ({ window }: { window: string }): string =>
49
+ WINDOW_WORDS[window] ?? window
50
+
51
+ /**
52
+ * The order windows are shown in: five-hour, weekly, the other weekly ones
53
+ * (Opus, Sonnet, ...), then the rest.
54
+ */
55
+ export const windowRank = ({ window }: { window: string }): number => {
56
+ if (window === 'five_hour') return 0
57
+ if (window === 'seven_day') return 1
58
+ if (window.startsWith('seven_day')) return 2
59
+ return 3
60
+ }
61
+
62
+ /** How much of one plan window something used, as a whole percent. */
63
+ export const PlanUsedSchema = z.object({
64
+ /** The window in words, such as "five-hour" or "weekly". */
65
+ window: z.string(),
66
+ percent: z.number(),
67
+ })
68
+
69
+ export type PlanUsed = z.infer<typeof PlanUsedSchema>
70
+
71
+ /**
72
+ * The plan used, in words, windows in their shown order.
73
+ *
74
+ * @example
75
+ * planUsedText({ used, sign: '+' }) // 'five-hour +1%, weekly +1%'
76
+ * planUsedText({ used, sign: '' }) // 'five-hour 3%, weekly 1%'
77
+ */
78
+ export const planUsedText = ({
79
+ used,
80
+ sign,
81
+ }: {
82
+ used: PlanUsed[]
83
+ sign: '+' | ''
84
+ }): string =>
85
+ used.map(({ window, percent }) => `${window} ${sign}${percent}%`).join(', ')
86
+
87
+ /**
88
+ * The plan's usage, from the rate-limit readings in agents' sessions. Each
89
+ * window keeps its latest reading; `null` until one arrives.
90
+ */
91
+ export const UsageSchema = z.object({
92
+ five_hour_percent: z.number().nullable(),
93
+ weekly_percent: z.number().nullable(),
94
+ five_hour_level: UsageLevelSchema.nullable(),
95
+ weekly_level: UsageLevelSchema.nullable(),
96
+ /** When the five-hour window resets. */
97
+ resets_at: z.string().nullable(),
98
+ read_at: z.string(),
99
+ })
100
+
101
+ export type Usage = z.infer<typeof UsageSchema>
102
+
103
+ /**
104
+ * The words on the plan usage: its windows are the whole account's, every
105
+ * run's and chat's, never one run's.
106
+ */
107
+ export const USAGE_LABEL = 'plan usage (account-wide)'