@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,372 @@
1
+ /**
2
+ * The run's **live file** (#628): `live.json` in the run folder, what each
3
+ * agent in a turn is doing now, and each check waiting for a prepare slot
4
+ * or a test slot (#632), for the board to show between journal records,
5
+ * and the run's last 50 actions across tickets, its live feed (#634). It
6
+ * is never journaled and never replayed: each write replaces
7
+ * the whole file (written aside, then renamed), at most once a second
8
+ * while something changed, and at least every 5 seconds while the engine
9
+ * runs, each with its `written_at` time. Each agent's entry carries its
10
+ * activity for each of the last 30 seconds (#631), for the board's
11
+ * heartbeat, and its helpers (#636), whose actions go in the feed too.
12
+ */
13
+ import { mkdirSync, renameSync, writeFileSync } from 'node:fs'
14
+ import { dirname, join } from 'node:path'
15
+
16
+ import { z } from 'zod'
17
+
18
+ import type { AgentLauncher } from '../agents/agent-launcher'
19
+ import type {
20
+ ProgressAction,
21
+ ProgressListener,
22
+ TurnProgress,
23
+ } from '../agents/turn-progress'
24
+ import type { SlotWaitListener } from '../gates/slot-waits'
25
+
26
+ /** The live file's name inside a run folder. */
27
+ export const LIVE_FILE = 'live.json'
28
+
29
+ /** The live file is written at most this often while something changes. */
30
+ export const LIVE_MIN_GAP_MS = 1000
31
+
32
+ /** The live file is written at least this often, changed or not. */
33
+ export const LIVE_MAX_GAP_MS = 5000
34
+
35
+ /** How often the writer looks for something to write. */
36
+ const LIVE_CHECK_MS = 200
37
+
38
+ /** How many seconds of activity each agent's entry keeps (#631). */
39
+ export const ACTIVITY_SECONDS = 30
40
+
41
+ /** The live file of one run: `<runs_dir>/<run_id>/live.json`. */
42
+ export const runLiveFilePath = ({
43
+ runs_dir,
44
+ run_id,
45
+ }: {
46
+ runs_dir: string
47
+ run_id: string
48
+ }): string => join(runs_dir, run_id, LIVE_FILE)
49
+
50
+ /** The live feed keeps this many of the run's latest actions (#634). */
51
+ export const LIVE_FEED_SIZE = 50
52
+
53
+ /** An action: a tool and a short target, with its time (ISO). */
54
+ const LiveActionSchema = z.object({
55
+ tool: z.string(),
56
+ target: z.string(),
57
+ at: z.string(),
58
+ })
59
+
60
+ /** A tool call whose result hasn't come back yet. */
61
+ const LiveOpenCallSchema = z.object({
62
+ tool: z.string(),
63
+ target: z.string(),
64
+ started_at: z.string(),
65
+ })
66
+
67
+ /** One of an agent's helpers (#636). */
68
+ const LiveHelperSchema = z.object({
69
+ model: z.string(),
70
+ action: LiveActionSchema.nullable(),
71
+ done: z.boolean(),
72
+ })
73
+
74
+ /** One agent in a turn, as the live file has it. */
75
+ const LiveAgentSchema = z.object({
76
+ ticket: z.number().int(),
77
+ role: z.string(),
78
+ model: z.string().nullable(),
79
+ tokens: z.number().min(0),
80
+ action: LiveActionSchema.nullable(),
81
+ open_call: LiveOpenCallSchema.nullable(),
82
+ /** The helpers the agent started this turn (#636). */
83
+ helpers: z.array(LiveHelperSchema),
84
+ /** One count for each of the last 30 seconds, oldest first (#631). */
85
+ activity: z.array(z.number().int().min(0)),
86
+ })
87
+
88
+ /** A ticket's check waiting for a slot (#632), as the live file has it. */
89
+ const LiveWaitSchema = z.object({
90
+ ticket: z.number().int(),
91
+ for: z.enum(['prepare_slot', 'test_slot']),
92
+ in_use: z.number().int().min(0),
93
+ slots: z.number().int().min(0),
94
+ since: z.string(),
95
+ })
96
+
97
+ /** One action of the live feed: who did it, on which model, and what. */
98
+ const LiveFeedEntrySchema = z.object({
99
+ at: z.string(),
100
+ ticket: z.number().int(),
101
+ role: z.string(),
102
+ model: z.string().nullable(),
103
+ tool: z.string(),
104
+ target: z.string(),
105
+ })
106
+
107
+ /** The whole live file, as each write has it. */
108
+ export const LiveFileSchema = z.object({
109
+ written_at: z.string(),
110
+ /** The run's first plan-usage reading, as its `rate_limit_event` sent it. */
111
+ plan_usage: z.record(z.string(), z.unknown()).nullable(),
112
+ agents: z.array(LiveAgentSchema),
113
+ waits: z.array(LiveWaitSchema),
114
+ feed: z.array(LiveFeedEntrySchema),
115
+ })
116
+
117
+ export type LiveFile = z.infer<typeof LiveFileSchema>
118
+
119
+ export type LiveAgent = z.infer<typeof LiveAgentSchema>
120
+
121
+ export type LiveHelper = z.infer<typeof LiveHelperSchema>
122
+
123
+ export type LiveWait = z.infer<typeof LiveWaitSchema>
124
+
125
+ type LiveFeedEntry = z.infer<typeof LiveFeedEntrySchema>
126
+
127
+ /** An agent's entry before its activity is added at write time. */
128
+ type HeldAgent = Omit<LiveAgent, 'activity'>
129
+
130
+ const sameAction = (
131
+ left: ProgressAction | null,
132
+ right: ProgressAction
133
+ ): boolean =>
134
+ left !== null &&
135
+ left.tool === right.tool &&
136
+ left.target === right.target &&
137
+ left.at === right.at
138
+
139
+ /**
140
+ * One agent's activity (#631): a count per second, by the second it
141
+ * happened in. Each progress heard is one tick, and so is each second a
142
+ * tool call is open, a low, steady ripple while a long command runs.
143
+ */
144
+ const createActivity = () => {
145
+ const counts = new Map<number, number>()
146
+ /** The last second an open tool call ticked in. */
147
+ let rippled = -1
148
+
149
+ const secondOf = (ms: number): number => Math.floor(ms / 1000)
150
+
151
+ const tick = (at: number): void => {
152
+ const second = secondOf(at)
153
+ counts.set(second, (counts.get(second) ?? 0) + 1)
154
+ for (const old of counts.keys()) {
155
+ if (old <= second - ACTIVITY_SECONDS) counts.delete(old)
156
+ }
157
+ }
158
+
159
+ /** One low tick for this second, once, while a tool call is open. */
160
+ const ripple = (at: number): boolean => {
161
+ if (secondOf(at) === rippled) return false
162
+ rippled = secondOf(at)
163
+ tick(at)
164
+ return true
165
+ }
166
+
167
+ /** The counts of the last 30 seconds up to `at`, oldest first. */
168
+ const recent = (at: number): number[] =>
169
+ Array.from(
170
+ { length: ACTIVITY_SECONDS },
171
+ (_, index) =>
172
+ counts.get(secondOf(at) - ACTIVITY_SECONDS + 1 + index) ?? 0
173
+ )
174
+
175
+ return { tick, ripple, recent }
176
+ }
177
+
178
+ /**
179
+ * Keeps the live file at `file` from the moment it is made until `close`,
180
+ * which writes it one last time with no agents. `launcher` wraps an agent
181
+ * launcher so each turn's progress (`on_progress`) goes into the file while
182
+ * the turn runs; `slotWaits` hears a ticket step's slot waits (see
183
+ * `hearSlotWaits`). A write that fails is skipped; the run never stops for it.
184
+ *
185
+ * @example
186
+ * const live = createLiveFile({ file: runLiveFilePath({ runs_dir, run_id }) })
187
+ * await runSteps({ launcher: live.launcher(launcher) })
188
+ * live.close()
189
+ */
190
+ export const createLiveFile = ({ file }: { file: string }) => {
191
+ const agents = new Map<number, HeldAgent>()
192
+ const activities = new Map<number, ReturnType<typeof createActivity>>()
193
+ const waits = new Map<number, LiveWait>()
194
+ let plan_usage: Record<string, unknown> | null = null
195
+ let feed: LiveFeedEntry[] = []
196
+ let changed = false
197
+ let last = 0
198
+ let next = 0
199
+
200
+ const write = (): void => {
201
+ const at = Date.now()
202
+ last = at
203
+ changed = false
204
+ const live: LiveFile = {
205
+ written_at: new Date(at).toISOString(),
206
+ plan_usage,
207
+ agents: [...agents].map(([key, agent]) => ({
208
+ ...agent,
209
+ activity: activities.get(key)?.recent(at) ?? [],
210
+ })),
211
+ waits: [...waits.values()],
212
+ feed,
213
+ }
214
+ const text = JSON.stringify(live)
215
+ try {
216
+ mkdirSync(dirname(file), { recursive: true })
217
+ const aside = `${file}.${process.pid}.tmp`
218
+ writeFileSync(aside, text)
219
+ renameSync(aside, file)
220
+ } catch {
221
+ // The live file is only for the board; the run goes on without it.
222
+ }
223
+ }
224
+
225
+ const timer = setInterval(() => {
226
+ const at = Date.now()
227
+ for (const [key, agent] of agents) {
228
+ if (agent.open_call === null) continue
229
+ if (activities.get(key)?.ripple(at)) changed = true
230
+ }
231
+ const gap = at - last
232
+ if ((changed && gap >= LIVE_MIN_GAP_MS) || gap >= LIVE_MAX_GAP_MS) {
233
+ write()
234
+ }
235
+ }, LIVE_CHECK_MS)
236
+ timer.unref?.()
237
+ write()
238
+
239
+ const heard = (key: number, progress: TurnProgress): void => {
240
+ const agent = agents.get(key)
241
+ if (agent === undefined) return
242
+ activities.get(key)?.tick(Date.now())
243
+ const { action, helpers = [] } = progress
244
+ const { ticket, role, model } = agent
245
+ // A message that only adds tokens repeats the action it was on.
246
+ const fresh = [
247
+ ...(action !== null && !sameAction(agent.action, action)
248
+ ? [{ ...action, ticket, role, model }]
249
+ : []),
250
+ // A helper's actions go in the feed too, on its own model.
251
+ ...helpers.flatMap((helper, index) =>
252
+ helper.action !== null &&
253
+ !sameAction(agent.helpers[index]?.action ?? null, helper.action)
254
+ ? [
255
+ {
256
+ ...helper.action,
257
+ ticket,
258
+ role: 'helper',
259
+ model: helper.model,
260
+ },
261
+ ]
262
+ : []
263
+ ),
264
+ ]
265
+ if (fresh.length > 0) feed = [...feed, ...fresh].slice(-LIVE_FEED_SIZE)
266
+ agents.set(key, {
267
+ ...agent,
268
+ tokens: progress.tokens,
269
+ action: progress.action,
270
+ open_call: progress.open_call,
271
+ helpers,
272
+ })
273
+ plan_usage ??= progress.rate_limit_events[0] ?? null
274
+ changed = true
275
+ }
276
+
277
+ /** Shows the agent `who` in the file while `turn` runs. */
278
+ const during = async <T>({
279
+ who,
280
+ on_progress,
281
+ turn,
282
+ }: {
283
+ who: Pick<HeldAgent, 'ticket' | 'role' | 'model'>
284
+ on_progress: ProgressListener | undefined
285
+ turn: (hear: ProgressListener) => Promise<T>
286
+ }): Promise<T> => {
287
+ const key = next
288
+ next += 1
289
+ agents.set(key, {
290
+ ...who,
291
+ tokens: 0,
292
+ action: null,
293
+ open_call: null,
294
+ helpers: [],
295
+ })
296
+ activities.set(key, createActivity())
297
+ changed = true
298
+ try {
299
+ return await turn((progress) => {
300
+ heard(key, progress)
301
+ on_progress?.(progress)
302
+ })
303
+ } finally {
304
+ agents.delete(key)
305
+ activities.delete(key)
306
+ changed = true
307
+ }
308
+ }
309
+
310
+ const launcher = <L extends AgentLauncher>(inner: L): L => {
311
+ // A follow-up runs on its session's model.
312
+ const models = new Map<string, string>()
313
+ return {
314
+ ...inner,
315
+ launch: async (args) => {
316
+ const turn = await during({
317
+ who: {
318
+ ticket: args.ticket,
319
+ role: args.role,
320
+ model: args.model,
321
+ },
322
+ on_progress: args.on_progress,
323
+ turn: (hear) =>
324
+ inner.launch({ ...args, on_progress: hear }),
325
+ })
326
+ if (turn.session_id !== undefined) {
327
+ models.set(turn.session_id, args.model)
328
+ }
329
+ return turn
330
+ },
331
+ followUp: (args) =>
332
+ during({
333
+ who: {
334
+ ticket: args.ticket,
335
+ role: args.role,
336
+ model: models.get(args.session_id) ?? null,
337
+ },
338
+ on_progress: args.on_progress,
339
+ turn: (hear) =>
340
+ inner.followUp({ ...args, on_progress: hear }),
341
+ }),
342
+ }
343
+ }
344
+
345
+ /** Shows each slot wait of `ticket`'s step in the file while it lasts. */
346
+ const slotWaits =
347
+ ({ ticket }: { ticket: number }): SlotWaitListener =>
348
+ (wait) => {
349
+ const key = next
350
+ next += 1
351
+ waits.set(key, {
352
+ ticket,
353
+ ...wait,
354
+ since: new Date().toISOString(),
355
+ })
356
+ changed = true
357
+ return () => {
358
+ waits.delete(key)
359
+ changed = true
360
+ }
361
+ }
362
+
363
+ const close = (): void => {
364
+ clearInterval(timer)
365
+ agents.clear()
366
+ activities.clear()
367
+ waits.clear()
368
+ write()
369
+ }
370
+
371
+ return { launcher, slotWaits, close }
372
+ }
@@ -11,11 +11,34 @@
11
11
  export const MAX_FIX_ROUNDS = 3
12
12
 
13
13
  /**
14
- * Times an implementer may send a test back as bad, each to a fresh
15
- * test-writer. The bounce after these makes the ticket stuck.
14
+ * Reruns a ticket (or the final review) gets when a writer answers
15
+ * `environment` (#613): each reruns the failed check without using a fix
16
+ * round. The `environment` answer after these makes it stuck.
17
+ */
18
+ export const MAX_ENVIRONMENT_RERUNS = 2
19
+
20
+ /**
21
+ * Times a test may be sent back as bad, each to a fresh test-writer: by the
22
+ * implementer, or by the engine when a mapped test passes with no `expect()`
23
+ * (#581). The two share this cap. The bounce after these makes the ticket
24
+ * stuck.
16
25
  */
17
26
  export const MAX_BAD_TEST_BOUNCES = 1
18
27
 
28
+ /**
29
+ * Times a ticket's tests may go back to its test-writer after a rebase
30
+ * (#489): the implementer sent a test back as bad, because tickets that
31
+ * joined the run branch made it wrong. The bad test after these makes the
32
+ * ticket stuck.
33
+ */
34
+ export const MAX_TEST_UPDATES = 2
35
+
36
+ /**
37
+ * The most tests a test-writer may map to one acceptance criterion (#581):
38
+ * the red check refuses more. Write the fewest tests that prove it.
39
+ */
40
+ export const MAX_TESTS_PER_CRITERION = 3
41
+
19
42
  /**
20
43
  * Engine failures in a row (the SDK crashed, or a follow-up's session was
21
44
  * gone) after which a ticket is stuck. Each one before it starts a fresh
@@ -6,7 +6,8 @@ import type { Tracker } from '../tracker/tracker'
6
6
  * again just before the PR opens (#484): a ticket reopened while the run
7
7
  * went, or one that was closed at intake and reopened since. The PR names
8
8
  * them (`withNotInRun`), so they are no surprise later. Oldest first, as
9
- * the tracker lists them.
9
+ * the tracker lists them. The tickets intake left out for a person (#499)
10
+ * aren't among them: the PR names those in their own sections.
10
11
  *
11
12
  * @example
12
13
  * const open = await openTicketsNotInRun({ tracker, state })
@@ -21,9 +22,12 @@ export const openTicketsNotInRun = async ({
21
22
  }): Promise<{ number: number; title: string }[]> => {
22
23
  const { spec_number, snapshot } = state
23
24
  if (spec_number === null) return []
24
- const inRun = new Set(snapshot?.ticket_order ?? [])
25
+ const known = new Set([
26
+ ...(snapshot?.ticket_order ?? []),
27
+ ...(snapshot?.left_out ?? []).map(({ number }) => number),
28
+ ])
25
29
  const tickets = await tracker.listSubTickets({ spec_number })
26
30
  return tickets
27
- .filter(({ number, state }) => state === 'open' && !inRun.has(number))
31
+ .filter(({ number, state }) => state === 'open' && !known.has(number))
28
32
  .map(({ number, title }) => ({ number, title }))
29
33
  }