@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,194 @@
1
+ import { realpathSync } from 'node:fs'
2
+ import { mkdir, mkdtemp, rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ import { $ } from 'bun'
7
+
8
+ import { afterEach, beforeEach, expect } from 'bun:test'
9
+
10
+ import type { FakeMuninn } from './fake-muninn'
11
+ import { git } from './practice-repo'
12
+
13
+ import { runSetup } from '../cli/setup'
14
+
15
+ /**
16
+ * `luca setup` end to end (seam 4): a throwaway repo with a local bare
17
+ * `origin`, a fake GitHub (labels, the `gh` login, the repo, and its issue
18
+ * links), and a fake MuninnDB. Shared by `cli/setup.test.ts` and
19
+ * `cli/setup-checks.test.ts`: each calls `useSetupRepo()` once, and reads
20
+ * `root`, `repo`, `origin`, and `logs` as they stand in the running test.
21
+ */
22
+
23
+ export const CONFIG_PATH = join('.luca', 'config.json')
24
+
25
+ /** The keys a new-style engine config may have. */
26
+ export const NEW_CONFIG_KEYS = [
27
+ 'checks',
28
+ 'test_file_patterns',
29
+ 'test_setup_files',
30
+ 'rule_files',
31
+ 'muninn',
32
+ 'run_budget_tokens',
33
+ ]
34
+
35
+ /** A `package.json` shaped like tmnb's: a bun test, a vitest file, and more. */
36
+ export const TMNB_PACKAGE = JSON.stringify(
37
+ {
38
+ name: 'tmnb',
39
+ private: true,
40
+ scripts: {
41
+ dev: 'next dev',
42
+ test: 'bun test',
43
+ 'test:workers': 'vitest run --config vitest.workers.config.ts',
44
+ 'type-check': 'tsc --noEmit',
45
+ lint: 'eslint .',
46
+ 'syncpack:check': 'syncpack lint',
47
+ },
48
+ },
49
+ null,
50
+ 4
51
+ )
52
+
53
+ /** An old-Luca config: its own keys, and the vault to keep. */
54
+ export const OLD_LUCA_CONFIG = JSON.stringify(
55
+ {
56
+ lucaVersion: '13.1.0-alpha.0',
57
+ oversight: 'full-auto',
58
+ preferences: { schemaVersion: 1 },
59
+ muninn: {
60
+ vault: 'tmnb',
61
+ todoBacklog: { vault: 'tmnb', rootId: 'x' },
62
+ },
63
+ },
64
+ null,
65
+ 2
66
+ )
67
+
68
+ type FakeLabel = { name: string; color: string; description: string }
69
+
70
+ /** A fake GitHub side for the repo: labels, the login, and issue links. */
71
+ export const fakeGitHub = ({
72
+ labels = [],
73
+ login = 'asibilia',
74
+ repo_name = 'asibilia/tmnb',
75
+ sub_issues = true,
76
+ dependencies = true,
77
+ }: {
78
+ labels?: FakeLabel[]
79
+ login?: string | null
80
+ repo_name?: string | null
81
+ sub_issues?: boolean
82
+ dependencies?: boolean
83
+ } = {}) => {
84
+ const store = labels.map((label) => ({ ...label }))
85
+ const created: string[] = []
86
+ return {
87
+ github: {
88
+ login: async () => login,
89
+ githubRepo: async () => repo_name,
90
+ issueLinks: async () => ({ sub_issues, dependencies }),
91
+ listLabels: async () => store.map(({ name }) => name),
92
+ createLabel: async ({ name, color, description }: FakeLabel) => {
93
+ if (store.some((label) => label.name === name)) {
94
+ throw new Error(`label ${name} already exists`)
95
+ }
96
+ created.push(name)
97
+ store.push({ name, color, description })
98
+ },
99
+ },
100
+ /** Every label now, as stored. */
101
+ labels: () => store.map((label) => ({ ...label })),
102
+ /** The names of the labels setup created, oldest first. */
103
+ created: () => [...created],
104
+ }
105
+ }
106
+
107
+ export let root = ''
108
+ export let repo = ''
109
+ export let origin = ''
110
+ export const logs: string[] = []
111
+ const log = (line: string) => {
112
+ logs.push(line)
113
+ }
114
+
115
+ /** Makes fresh throwaway folders before each test, and removes them after. */
116
+ export const useSetupRepo = () => {
117
+ beforeEach(async () => {
118
+ root = realpathSync(await mkdtemp(join(tmpdir(), 'setup-repo-')))
119
+ repo = join(root, 'repo')
120
+ origin = join(root, 'origin.git')
121
+ logs.length = 0
122
+ })
123
+
124
+ afterEach(async () => {
125
+ await rm(root, { recursive: true, force: true })
126
+ })
127
+ }
128
+
129
+ /**
130
+ * A throwaway repo with these files in its first commit and a local bare
131
+ * `origin`. `main` is pushed unless `push` is false.
132
+ */
133
+ export const makeRepo = async ({
134
+ files,
135
+ push = true,
136
+ }: {
137
+ files: Record<string, string>
138
+ push?: boolean
139
+ }) => {
140
+ await $`git init -q --bare -b main ${origin}`.quiet()
141
+ await $`git init -q -b main ${repo}`.quiet()
142
+ const hooks = join(root, 'no-hooks')
143
+ await mkdir(hooks)
144
+ await git(repo, 'config', 'user.name', 'Practice')
145
+ await git(repo, 'config', 'user.email', 'practice@example.com')
146
+ await git(repo, 'config', 'commit.gpgsign', 'false')
147
+ await git(repo, 'config', 'core.hooksPath', hooks)
148
+ await Bun.write(join(repo, 'README.md'), '# Throwaway\n')
149
+ for (const [path, content] of Object.entries(files)) {
150
+ await Bun.write(join(repo, path), content)
151
+ }
152
+ await git(repo, 'add', '-A')
153
+ await git(repo, 'commit', '-q', '-m', 'initial')
154
+ await git(repo, 'remote', 'add', 'origin', origin)
155
+ if (push) await git(repo, 'push', '-q', 'origin', 'main')
156
+ }
157
+
158
+ export const setup = async ({
159
+ github,
160
+ memory,
161
+ }: {
162
+ github: ReturnType<typeof fakeGitHub>['github']
163
+ memory: FakeMuninn
164
+ }) => runSetup({ repo, github, memory, log })
165
+
166
+ /** Everything the command printed, its end message included. */
167
+ export const printed = (end: { message: string }): string =>
168
+ [...logs, end.message].join('\n')
169
+
170
+ export const readConfigText = (): Promise<string> =>
171
+ Bun.file(join(repo, CONFIG_PATH)).text()
172
+
173
+ export const checkNamed = (
174
+ end: Awaited<ReturnType<typeof setup>>,
175
+ name: string
176
+ ): { name: string; status: string; detail: string } => {
177
+ const found = end.checks.find((check) => check.name === name)
178
+ if (found === undefined) throw new Error(`No check named ${name}`)
179
+ return found
180
+ }
181
+
182
+ /** Checks that a check is a to-do whose fix is printed. */
183
+ export const expectToDo = (
184
+ end: Awaited<ReturnType<typeof setup>>,
185
+ name: string
186
+ ) => {
187
+ const check = checkNamed(end, name)
188
+ expect(check.status).toBe('todo')
189
+ expect(check.detail.trim()).not.toBe('')
190
+ expect(printed(end)).toContain(check.detail)
191
+ }
192
+
193
+ export const commitCount = async (cwd: string): Promise<string> =>
194
+ (await git(cwd, 'rev-list', '--count', 'main')).trim()
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Journal entry builders for the scheduler's step records: a step started
3
+ * and a step ended.
4
+ */
5
+ import type { AgentRole } from '../agents/role-results'
6
+ import type { JournalEntry } from '../journal/journal-record'
7
+
8
+ /**
9
+ * The scheduler started a step: `step` is its action type, plus `:<role>`
10
+ * for an agent's turn. `key` defaults to the ticket's number, or `run`.
11
+ */
12
+ export const stepStarted = ({
13
+ ticket,
14
+ step,
15
+ key,
16
+ role,
17
+ first_seq,
18
+ }: {
19
+ ticket: number | null
20
+ step: string
21
+ key?: string
22
+ role?: AgentRole
23
+ /** The step's first try, on a redo. Defaults to `null`. */
24
+ first_seq?: number
25
+ }): JournalEntry => ({
26
+ kind: 'step_started',
27
+ ticket,
28
+ role: role ?? null,
29
+ content: {
30
+ key: key ?? (ticket === null ? 'run' : String(ticket)),
31
+ step,
32
+ first_seq: first_seq ?? null,
33
+ },
34
+ })
35
+
36
+ /** The scheduler saw a step settle. `key` as in `stepStarted`. */
37
+ export const stepEnded = ({
38
+ ticket,
39
+ step,
40
+ key,
41
+ role,
42
+ }: {
43
+ ticket: number | null
44
+ step: string
45
+ key?: string
46
+ role?: AgentRole
47
+ }): JournalEntry => ({
48
+ kind: 'step_ended',
49
+ ticket,
50
+ role: role ?? null,
51
+ content: {
52
+ key: key ?? (ticket === null ? 'run' : String(ticket)),
53
+ step,
54
+ },
55
+ })
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Journal entry builders for the steps along a ticket's spine: the red check,
3
+ * the leftover scan, commits, gates, resets, the join and its rebase, the
4
+ * push, a stuck ticket, and `ticketBuilt`, every step of one ticket passing.
5
+ */
6
+ import { implemented, reviewed, testsWritten } from './agent-turn-fixtures'
7
+ import {
8
+ baselineTests,
9
+ emptyTestRun,
10
+ RUN_BRANCH,
11
+ ticketWorktreeCreated,
12
+ } from './run-fixtures'
13
+
14
+ import type { BadTest } from '../agents/role-results'
15
+ import type { TestCase } from '../gates/gate-schemas'
16
+ import type {
17
+ CommitStage,
18
+ JournalEntry,
19
+ StuckReason,
20
+ } from '../journal/journal-record'
21
+
22
+ export const redCheck = ({
23
+ ticket,
24
+ ok,
25
+ }: {
26
+ ticket: number
27
+ ok: boolean
28
+ }): JournalEntry => ({
29
+ kind: 'red_check',
30
+ ticket,
31
+ role: null,
32
+ content: {
33
+ ok,
34
+ problems: ok ? [] : ['"sum adds two numbers" passes already'],
35
+ notes: [],
36
+ tests: {
37
+ ...emptyTestRun(),
38
+ output: ok ? '1 fail' : '(pass) sum adds two numbers',
39
+ },
40
+ },
41
+ })
42
+
43
+ export const leftoverScan = ({
44
+ ticket,
45
+ stage,
46
+ hits,
47
+ }: {
48
+ /** `null` for the final review. */
49
+ ticket: number | null
50
+ stage: CommitStage
51
+ hits?: { path: string; reason: string }[]
52
+ }): JournalEntry => ({
53
+ kind: 'leftover_scan',
54
+ ticket,
55
+ role: null,
56
+ content: { stage, hits: hits ?? [] },
57
+ })
58
+
59
+ export const commitMade = ({
60
+ ticket,
61
+ stage,
62
+ sha,
63
+ files,
64
+ }: {
65
+ /** `null` for the final review. */
66
+ ticket: number | null
67
+ stage: CommitStage
68
+ /** Defaults to `<stage>-sha`. */
69
+ sha?: string
70
+ files?: string[]
71
+ }): JournalEntry => ({
72
+ kind: 'commit_made',
73
+ ticket,
74
+ role: null,
75
+ content: {
76
+ stage,
77
+ sha: sha ?? `${stage}-sha`,
78
+ message: stage,
79
+ files: files ?? [],
80
+ },
81
+ })
82
+
83
+ export const gatesRun = ({
84
+ ticket,
85
+ target,
86
+ ok,
87
+ cases,
88
+ }: {
89
+ /** `null` for the final review. */
90
+ ticket: number | null
91
+ target: 'ticket' | 'run_branch'
92
+ ok: boolean
93
+ /** The test cases a ticket's gates journal (#581). Defaults to none. */
94
+ cases?: TestCase[]
95
+ }): JournalEntry => ({
96
+ kind: 'gates_run',
97
+ ticket,
98
+ role: null,
99
+ content: {
100
+ target,
101
+ ok,
102
+ ...(cases === undefined ? {} : { cases }),
103
+ checks: [
104
+ {
105
+ name: 'test',
106
+ command: 'bun test',
107
+ ok,
108
+ exit_code: ok ? 0 : 1,
109
+ output: ok ? '' : '1 fail',
110
+ },
111
+ ],
112
+ },
113
+ })
114
+
115
+ /**
116
+ * The engine threw away a ticket worktree's uncommitted changes. With a
117
+ * `bad_test`, the engine found that test checks nothing (#581).
118
+ */
119
+ export const worktreeReset = ({
120
+ ticket,
121
+ bad_test,
122
+ }: {
123
+ ticket: number
124
+ bad_test?: BadTest
125
+ }): JournalEntry => ({
126
+ kind: 'worktree_reset',
127
+ ticket,
128
+ role: null,
129
+ content: {
130
+ sha: 'red-sha',
131
+ ...(bad_test === undefined ? {} : { bad_test }),
132
+ },
133
+ })
134
+
135
+ export const joined = ({ ticket }: { ticket: number }): JournalEntry => ({
136
+ kind: 'ticket_joined',
137
+ ticket,
138
+ role: null,
139
+ content: { ok: true, shas: ['r1', 'g1'] },
140
+ })
141
+
142
+ /** A join whose cherry-pick clashed with the run branch (git aborted it). */
143
+ export const joinClashed = ({
144
+ ticket,
145
+ error,
146
+ }: {
147
+ ticket: number
148
+ /** Defaults to a conflict in `src/index.ts`. */
149
+ error?: string
150
+ }): JournalEntry => ({
151
+ kind: 'ticket_joined',
152
+ ticket,
153
+ role: null,
154
+ content: { ok: false, error: error ?? 'CONFLICT (content): src/index.ts' },
155
+ })
156
+
157
+ /** The engine put a ticket's change back on top of the run branch. */
158
+ export const ticketRebased = ({
159
+ ticket,
160
+ cause,
161
+ tests,
162
+ code,
163
+ undone,
164
+ }: {
165
+ ticket: number
166
+ cause: 'clash' | 'join_gates'
167
+ /** The test files that clashed. Defaults to none. */
168
+ tests?: string[]
169
+ /** The code files that clashed. Defaults to none. */
170
+ code?: string[]
171
+ /** The run branch commits undone first. Defaults to none. */
172
+ undone?: string[]
173
+ }): JournalEntry => ({
174
+ kind: 'ticket_rebased',
175
+ ticket,
176
+ role: null,
177
+ content: {
178
+ cause,
179
+ base_sha: 'onto-sha',
180
+ tests: tests ?? [],
181
+ code: code ?? [],
182
+ undone: undone ?? [],
183
+ },
184
+ })
185
+
186
+ /**
187
+ * After a rebase, the engine sent a ticket's tests back to its test-writer
188
+ * (#489): #12 "Add product" joined, and `src/index.ts` changed.
189
+ */
190
+ export const testsSentBack = ({
191
+ ticket,
192
+ round,
193
+ reason,
194
+ }: {
195
+ ticket: number
196
+ /** Defaults to 1. */
197
+ round?: number
198
+ /** The bad test's reason. Defaults to "Wrong sum." */
199
+ reason?: string
200
+ }): JournalEntry => ({
201
+ kind: 'tests_sent_back',
202
+ ticket,
203
+ role: null,
204
+ content: {
205
+ round: round ?? 1,
206
+ bad_test: {
207
+ file: 'src/sum.test.ts',
208
+ name: 'sum adds two numbers',
209
+ reason: reason ?? 'Wrong sum.',
210
+ },
211
+ from_sha: 'b0',
212
+ base_sha: 'onto-sha',
213
+ joined: [{ ticket: 12, title: 'Add product' }],
214
+ files: ['src/index.ts', 'src/product.ts'],
215
+ },
216
+ })
217
+
218
+ export const ticketStuck = ({
219
+ ticket,
220
+ reason,
221
+ detail,
222
+ }: {
223
+ ticket: number
224
+ reason: StuckReason
225
+ /** Defaults to "why". */
226
+ detail?: string
227
+ }): JournalEntry => ({
228
+ kind: 'ticket_stuck',
229
+ ticket,
230
+ role: null,
231
+ content: { reason, detail: detail ?? 'why' },
232
+ })
233
+
234
+ export const pushed = ({
235
+ ticket,
236
+ sha,
237
+ }: {
238
+ /** `null` for the final review's push. */
239
+ ticket: number | null
240
+ /** Defaults to `g1`. */
241
+ sha?: string
242
+ }): JournalEntry => ({
243
+ kind: 'run_branch_pushed',
244
+ ticket,
245
+ role: null,
246
+ content: { branch: RUN_BRANCH, sha: sha ?? 'g1' },
247
+ })
248
+
249
+ /** Every step of one ticket, from its worktree to the push, all passing. */
250
+ export const ticketBuilt = ({ ticket }: { ticket: number }): JournalEntry[] => [
251
+ ticketWorktreeCreated({ ticket }),
252
+ baselineTests({ ticket }),
253
+ testsWritten({ ticket }),
254
+ redCheck({ ticket, ok: true }),
255
+ leftoverScan({ ticket, stage: 'red' }),
256
+ commitMade({ ticket, stage: 'red' }),
257
+ implemented({ ticket }),
258
+ gatesRun({ ticket, target: 'ticket', ok: true }),
259
+ leftoverScan({ ticket, stage: 'green' }),
260
+ commitMade({ ticket, stage: 'green' }),
261
+ reviewed({ ticket }),
262
+ joined({ ticket }),
263
+ gatesRun({ ticket, target: 'run_branch', ok: true }),
264
+ pushed({ ticket }),
265
+ ]
266
+
267
+ /** One ticket's steps up to and including its approving review. */
268
+ export const ticketApproved = ({
269
+ ticket,
270
+ }: {
271
+ ticket: number
272
+ }): JournalEntry[] => ticketBuilt({ ticket }).slice(0, 11)
@@ -0,0 +1,186 @@
1
+ import { ticketIssue } from './intake-fixtures'
2
+ import {
3
+ journalRecords,
4
+ waitUntil,
5
+ type ManyTicketsScenario,
6
+ } from './many-tickets'
7
+ import {
8
+ APPROVE,
9
+ IMPLEMENTER_RESULT,
10
+ PRACTICE_SPEC_NUMBER,
11
+ practiceSpec,
12
+ SUM,
13
+ SUM_TEST,
14
+ TEST_WRITER_RESULT,
15
+ } from './practice-repo'
16
+ import { AVERAGE, AVERAGE_TEST } from './practice-run'
17
+
18
+ import type { ScriptedTurn } from '../agents/scripted-launcher'
19
+ import { createInMemoryTracker } from '../tracker/in-memory-tracker'
20
+
21
+ /**
22
+ * A many-ticket run with all three waits between steps (#559): #21 "Add
23
+ * sum" and #22 "Add product" start from the same run-branch commit, so one
24
+ * waits for the other's baseline tests. #21's reviewer approves once #22's
25
+ * reviewer has started, and #22's reviewer approves once #21 has joined,
26
+ * so #22 is approved while #21's checks run on the run branch: it waits to
27
+ * join after #21. #23 "Add average" waits on #21; once #21 is pushed, #22
28
+ * is still in the join queue, so #23 starts once #22 has joined. No ticket
29
+ * touches another's files, so no join clashes.
30
+ */
31
+
32
+ const PRODUCT_TEST = `import { describe, expect, test } from 'bun:test'
33
+
34
+ import { product } from './product'
35
+
36
+ describe('product', () => {
37
+ test('multiplies two numbers', () => {
38
+ expect(product({ numbers: [2, 3] })).toBe(6)
39
+ })
40
+
41
+ test('of no numbers is one', () => {
42
+ expect(product({ numbers: [] })).toBe(1)
43
+ })
44
+ })
45
+ `
46
+
47
+ const PRODUCT = `export const product = ({ numbers }: { numbers: number[] }): number =>
48
+ numbers.reduce((total, each) => total * each, 1)
49
+ `
50
+
51
+ /** A test-writer's result mapping AC1 and AC2 to the tests of `file`. */
52
+ const mapped = ({
53
+ file,
54
+ names,
55
+ }: {
56
+ file: string
57
+ names: [string, string]
58
+ }) => ({
59
+ outcome: 'tests_written',
60
+ criteria: names.map((name, index) => ({
61
+ criterion_id: `AC${index + 1}`,
62
+ tests: [{ file, name }],
63
+ })),
64
+ summary: 'One test per criterion.',
65
+ assumptions: [],
66
+ run_notes: [],
67
+ })
68
+
69
+ /** The baseline, join and start waits, in one run of #21, #22 and #23. */
70
+ export const THREE_WAITS: ManyTicketsScenario = {
71
+ tracker: () =>
72
+ createInMemoryTracker({
73
+ issues: [
74
+ practiceSpec(),
75
+ ticketIssue({
76
+ number: 21,
77
+ title: 'Add sum',
78
+ criteria: [
79
+ 'sum adds two numbers',
80
+ 'sum of no numbers is zero',
81
+ ],
82
+ }),
83
+ ticketIssue({
84
+ number: 22,
85
+ title: 'Add product',
86
+ criteria: [
87
+ 'product multiplies two numbers',
88
+ 'product of no numbers is one',
89
+ ],
90
+ }),
91
+ ticketIssue({
92
+ number: 23,
93
+ title: 'Add average',
94
+ criteria: [
95
+ 'average of two numbers is the one between them',
96
+ 'average of no numbers is zero',
97
+ ],
98
+ blocked_by_section: '- #21',
99
+ blocked_by: [21],
100
+ }),
101
+ ],
102
+ sub_tickets: { [PRACTICE_SPEC_NUMBER]: [21, 22, 23] },
103
+ }),
104
+ turns: ({ journal_file }): ScriptedTurn[] => {
105
+ const reviewing = new Set<number>()
106
+ return [
107
+ {
108
+ role: 'test-writer',
109
+ ticket: 21,
110
+ files: { 'src/sum.test.ts': SUM_TEST },
111
+ result: TEST_WRITER_RESULT,
112
+ },
113
+ {
114
+ role: 'test-writer',
115
+ ticket: 22,
116
+ files: { 'src/product.test.ts': PRODUCT_TEST },
117
+ result: mapped({
118
+ file: 'src/product.test.ts',
119
+ names: [
120
+ 'product > multiplies two numbers',
121
+ 'product > of no numbers is one',
122
+ ],
123
+ }),
124
+ },
125
+ {
126
+ role: 'implementer',
127
+ ticket: 21,
128
+ files: { 'src/sum.ts': SUM },
129
+ result: IMPLEMENTER_RESULT,
130
+ },
131
+ {
132
+ role: 'implementer',
133
+ ticket: 22,
134
+ files: { 'src/product.ts': PRODUCT },
135
+ result: { ...IMPLEMENTER_RESULT, summary: 'Added product.' },
136
+ },
137
+ {
138
+ role: 'ticket-reviewer',
139
+ ticket: 21,
140
+ act: async () =>
141
+ waitUntil({
142
+ check: () => reviewing.has(22),
143
+ what: "#22's reviewer to start",
144
+ }),
145
+ result: APPROVE,
146
+ },
147
+ {
148
+ role: 'ticket-reviewer',
149
+ ticket: 22,
150
+ act: async () => {
151
+ reviewing.add(22)
152
+ await waitUntil({
153
+ check: () =>
154
+ journalRecords(journal_file).some(
155
+ (record) =>
156
+ record.kind === 'ticket_joined' &&
157
+ record.ticket === 21 &&
158
+ record.content.ok
159
+ ),
160
+ what: "#21's join",
161
+ })
162
+ },
163
+ result: APPROVE,
164
+ },
165
+ {
166
+ role: 'test-writer',
167
+ ticket: 23,
168
+ files: { 'src/average.test.ts': AVERAGE_TEST },
169
+ result: mapped({
170
+ file: 'src/average.test.ts',
171
+ names: [
172
+ 'average > of two numbers is the one between them',
173
+ 'average > of no numbers is zero',
174
+ ],
175
+ }),
176
+ },
177
+ {
178
+ role: 'implementer',
179
+ ticket: 23,
180
+ files: { 'src/average.ts': AVERAGE },
181
+ result: { ...IMPLEMENTER_RESULT, summary: 'Added average.' },
182
+ },
183
+ { role: 'ticket-reviewer', ticket: 23, result: APPROVE },
184
+ ]
185
+ },
186
+ }