@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,260 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
2
+ import { mkdir, rm } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
+
5
+ import uniq from 'lodash/uniq'
6
+
7
+ import { refsMadeInTurn, type GitState } from './after-turn-check'
8
+ import { git, gitOk } from './worktree-git'
9
+ import type { SavedGit, TurnSnapshot } from './worktree-snapshot'
10
+
11
+ import { FROZEN_INSTALL } from '../gates/lockfile-install'
12
+ import { runShell } from '../shell/run-command'
13
+
14
+ /**
15
+ * Takes a ref an agent made off, keeping its commit under `refs/luca-undone/`
16
+ * so nothing is lost: the refs are shared, and the engine can't tell who
17
+ * made one. A ref that existed before goes back to where it was.
18
+ */
19
+ const undoRef = async ({
20
+ cwd,
21
+ ref,
22
+ before,
23
+ }: {
24
+ cwd: string
25
+ ref: string
26
+ before: string | undefined
27
+ }): Promise<void> => {
28
+ const now = await git({ cwd, args: ['rev-parse', '--verify', '-q', ref] })
29
+ if (!now.ok) return
30
+ const object = now.stdout.trim()
31
+ if (before !== undefined) {
32
+ await gitOk({ cwd, args: ['update-ref', ref, before, object] })
33
+ return
34
+ }
35
+ const kept = ref.replace(/^refs\//, 'refs/luca-undone/')
36
+ await gitOk({ cwd, args: ['update-ref', kept, object] })
37
+ await gitOk({ cwd, args: ['update-ref', '-d', ref, object] })
38
+ }
39
+
40
+ /**
41
+ * Drops the stash entries an agent made. The stash is shared, so each is
42
+ * found by its commit just before it is dropped, newest first.
43
+ */
44
+ const dropStashes = async ({
45
+ cwd,
46
+ entries,
47
+ }: {
48
+ cwd: string
49
+ entries: string[]
50
+ }): Promise<void> => {
51
+ const shas = entries.map((entry) => entry.split(' ')[0] ?? '')
52
+ for (const sha of shas) {
53
+ const list = await git({ cwd, args: ['stash', 'list', '--format=%H'] })
54
+ const index = list.stdout.split('\n').indexOf(sha)
55
+ if (index !== -1) {
56
+ await gitOk({
57
+ cwd,
58
+ args: ['stash', 'drop', '-q', `stash@{${index}}`],
59
+ })
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Whether HEAD is now a commit made during the turn: it moved, to a commit
66
+ * no ref named before the turn and that the old HEAD doesn't contain.
67
+ */
68
+ export const headIsNew = async ({
69
+ cwd,
70
+ before,
71
+ after,
72
+ saved,
73
+ }: {
74
+ cwd: string
75
+ before: GitState
76
+ after: GitState
77
+ saved: SavedGit
78
+ }): Promise<boolean> => {
79
+ if (after.head === before.head || after.head === '') return false
80
+ if (Object.values(saved.refs).includes(after.head)) return false
81
+ const older = await git({
82
+ cwd,
83
+ args: ['merge-base', '--is-ancestor', after.head, before.head],
84
+ })
85
+ return !older.ok
86
+ }
87
+
88
+ /**
89
+ * Puts HEAD, the branch, the index, the refs the agent made, and stashes
90
+ * back; the files stay.
91
+ */
92
+ export const restoreGit = async ({
93
+ cwd,
94
+ branch,
95
+ before,
96
+ after,
97
+ saved,
98
+ head_is_new,
99
+ }: {
100
+ cwd: string
101
+ branch: string
102
+ before: GitState
103
+ after: GitState
104
+ saved: SavedGit
105
+ head_is_new: boolean
106
+ }): Promise<void> => {
107
+ if (before.head_ref !== null) {
108
+ await gitOk({ cwd, args: ['symbolic-ref', 'HEAD', before.head_ref] })
109
+ }
110
+ // A mixed reset: HEAD and the branch go back, the index is emptied of
111
+ // the agent's staging, and the files in the worktree are left alone.
112
+ await gitOk({ cwd, args: ['reset', '-q', before.head] })
113
+ if (before.branch_ref !== null) {
114
+ await gitOk({
115
+ cwd,
116
+ args: ['update-ref', `refs/heads/${branch}`, before.branch_ref],
117
+ })
118
+ }
119
+ for (const ref of refsMadeInTurn({ before, after, head_is_new })) {
120
+ await undoRef({ cwd, ref, before: saved.refs[ref] })
121
+ }
122
+ await dropStashes({
123
+ cwd,
124
+ entries: after.stash.filter((entry) => !before.stash.includes(entry)),
125
+ })
126
+ }
127
+
128
+ /** Puts one path back how it was before the turn. */
129
+ export const restorePath = async ({
130
+ cwd,
131
+ path,
132
+ before,
133
+ }: {
134
+ cwd: string
135
+ path: string
136
+ before: TurnSnapshot
137
+ }): Promise<void> => {
138
+ const full = join(cwd, path)
139
+ const bytes = before.saved[path]
140
+ if (bytes !== undefined && bytes !== null) {
141
+ await mkdir(dirname(full), { recursive: true })
142
+ await rm(full, { force: true, recursive: true })
143
+ await Bun.write(full, bytes)
144
+ return
145
+ }
146
+ if (bytes === undefined) {
147
+ const tracked = await git({
148
+ cwd,
149
+ args: ['cat-file', '-e', `${before.git.head}:${path}`],
150
+ })
151
+ if (tracked.ok) {
152
+ await rm(full, { force: true, recursive: true })
153
+ await gitOk({
154
+ cwd,
155
+ args: ['checkout', before.git.head, '--', path],
156
+ })
157
+ await gitOk({ cwd, args: ['reset', '-q', '--', path] })
158
+ return
159
+ }
160
+ }
161
+ await rm(full, { force: true, recursive: true })
162
+ await pruneEmptyFolders({ cwd, path })
163
+ }
164
+
165
+ /** Removes the folders a new path left empty, up to the worktree. */
166
+ const pruneEmptyFolders = async ({
167
+ cwd,
168
+ path,
169
+ }: {
170
+ cwd: string
171
+ path: string
172
+ }): Promise<void> => {
173
+ for (let folder = dirname(path); folder !== '.'; folder = dirname(folder)) {
174
+ const full = join(cwd, folder)
175
+ if (!existsSync(full) || readdirSync(full).length > 0) return
176
+ await rm(full, { recursive: true, force: true })
177
+ }
178
+ }
179
+
180
+ /**
181
+ * The package folder a path under `node_modules` belongs to, such as
182
+ * `node_modules/@scope/name` for `node_modules/@scope/name/lib/a.js`.
183
+ */
184
+ const packageFolder = (path: string): string => {
185
+ const parts = path.split('/')
186
+ const at = parts.lastIndexOf('node_modules')
187
+ const scoped = parts[at + 1]?.startsWith('@') === true
188
+ return parts.slice(0, at + (scoped ? 3 : 2)).join('/')
189
+ }
190
+
191
+ /**
192
+ * Puts back files an agent changed or deleted under `node_modules`. Their
193
+ * bytes are not kept (there are too many), so each package they belong to
194
+ * is removed and the engine's own install puts it back from the lockfile.
195
+ *
196
+ * @returns Why the install failed, or `null`.
197
+ */
198
+ export const reinstallPackages = async ({
199
+ cwd,
200
+ paths,
201
+ }: {
202
+ cwd: string
203
+ paths: string[]
204
+ }): Promise<string | null> => {
205
+ for (const folder of uniq(paths.map(packageFolder))) {
206
+ await rm(join(cwd, folder), { force: true, recursive: true })
207
+ }
208
+ const result = await runShell({ command: FROZEN_INSTALL, cwd })
209
+ return result.exit_code === 0
210
+ ? null
211
+ : `the engine's install (${FROZEN_INSTALL}) failed, so node_modules may still hold what the agent changed:\n${result.stderr || result.stdout}`
212
+ }
213
+
214
+ /**
215
+ * Puts the ticket branch's own config section back, key by key: each of its
216
+ * keys now there is unset, then each saved entry is added back in order.
217
+ * Every other key in the shared `.git/config` stays as it is now, since
218
+ * other processes write there too. A key with no value comes back as
219
+ * `true`, which git reads the same.
220
+ *
221
+ * @returns Why the undo failed (such as another process holding the
222
+ * config's lock), or `null`.
223
+ */
224
+ export const restoreBranchConfig = async ({
225
+ cwd,
226
+ before,
227
+ after,
228
+ common_dir,
229
+ }: {
230
+ cwd: string
231
+ before: GitState
232
+ after: GitState
233
+ common_dir: string
234
+ }): Promise<string | null> => {
235
+ const file = join(common_dir, 'config')
236
+ const steps = [
237
+ ...uniq(after.branch_config.map(({ key }) => key)).map((key) => [
238
+ 'config',
239
+ '--file',
240
+ file,
241
+ '--unset-all',
242
+ key,
243
+ ]),
244
+ ...before.branch_config.map(({ key, value }) => [
245
+ 'config',
246
+ '--file',
247
+ file,
248
+ '--add',
249
+ key,
250
+ value ?? 'true',
251
+ ]),
252
+ ]
253
+ for (const args of steps) {
254
+ const result = await git({ cwd, args })
255
+ if (!result.ok) {
256
+ return `git ${args.join(' ')} failed, so the ticket branch's settings in the shared .git/config may still hold what the agent changed`
257
+ }
258
+ }
259
+ return null
260
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Pure: which worktree paths each role may create, change, or delete.
3
+ */
4
+ import { basename, isAbsolute, normalize, relative, resolve } from 'node:path'
5
+
6
+ import { isWriter, type GuardArgs } from './guard-roles'
7
+
8
+ import {
9
+ featureMapPath,
10
+ isFeatureMapFolder,
11
+ type EngineConfig,
12
+ } from '../config/engine-config'
13
+ import { testFilesAmong } from '../gates/test-runner'
14
+
15
+ /**
16
+ * Worktree-relative, `..` resolved. `null` when the path leaves the worktree.
17
+ * macOS reaches temp folders through `/var` and `/private/var` alike, so both
18
+ * sides drop the `/private` prefix before they are compared.
19
+ */
20
+ export const insideWorktree = ({
21
+ worktree,
22
+ path,
23
+ }: {
24
+ worktree: string
25
+ path: string
26
+ }): string | null => {
27
+ const canonical = (text: string) =>
28
+ text.replace(/^\/private\/(var|tmp)(?=\/|$)/, '/$1')
29
+ const root = canonical(resolve(worktree))
30
+ const full = canonical(resolve(root, path))
31
+ const inner = relative(root, full)
32
+ if (inner.startsWith('..') || isAbsolute(inner)) return null
33
+ return inner === '' ? '.' : inner
34
+ }
35
+
36
+ const isTestFile = ({
37
+ path,
38
+ config,
39
+ }: {
40
+ path: string
41
+ config: EngineConfig
42
+ }): boolean =>
43
+ testFilesAmong({
44
+ files: [path],
45
+ test_file_patterns: config.test_file_patterns,
46
+ }).length > 0
47
+
48
+ /**
49
+ * Whether a clean worktree-relative path is part of the repo's feature map
50
+ * (#583): the map file itself, or, when the map is a folder (#597), any
51
+ * `*.md` page under it.
52
+ */
53
+ const isFeatureMapPage = ({
54
+ path,
55
+ config,
56
+ }: {
57
+ path: string
58
+ config: EngineConfig
59
+ }): boolean => {
60
+ const map = featureMapPath({ config })
61
+ if (map === null) return false
62
+ return isFeatureMapFolder({ path: map })
63
+ ? path.startsWith(`${map}/`) && path.endsWith('.md')
64
+ : path === map
65
+ }
66
+
67
+ /**
68
+ * Whether a clean worktree-relative path matches the repo's
69
+ * `test_helper_patterns` (#619): a non-test file the test-writer may write.
70
+ */
71
+ export const isTestHelper = ({
72
+ path,
73
+ config,
74
+ }: {
75
+ path: string
76
+ config: EngineConfig
77
+ }): boolean =>
78
+ (config.test_helper_patterns ?? []).some((pattern) =>
79
+ new Bun.Glob(pattern).match(path)
80
+ )
81
+
82
+ /** The lockfiles a package install writes. The engine owns them. */
83
+ export const LOCKFILES = [
84
+ 'bun.lock',
85
+ 'bun.lockb',
86
+ 'package-lock.json',
87
+ 'npm-shrinkwrap.json',
88
+ 'yarn.lock',
89
+ 'pnpm-lock.yaml',
90
+ ]
91
+
92
+ /**
93
+ * Whether a worktree-relative path is the package install's: a lockfile, or
94
+ * anything under a `node_modules` folder. Only the engine's install gate
95
+ * writes these; no agent does.
96
+ *
97
+ * @example
98
+ * isInstallPath('bun.lock') // true
99
+ * isInstallPath('packages/app/node_modules/zod/index.js') // true
100
+ * isInstallPath('src/sum.ts') // false
101
+ */
102
+ export const isInstallPath = (path: string): boolean =>
103
+ LOCKFILES.includes(basename(path)) ||
104
+ normalize(path).split('/').includes('node_modules')
105
+
106
+ /**
107
+ * Whether an agent may create, change, or delete a worktree-relative path.
108
+ * Test-writers write test files only, the feature map when the repo has
109
+ * one (#583), or any page of a folder map (#597), and files matching
110
+ * `test_helper_patterns` (#619); implementers write anything but test
111
+ * files (unless they may edit tests) and test setup files; reviewers and the
112
+ * learner write nothing. No role writes a test setup file, `.git`, what
113
+ * the package install writes (lockfiles, `node_modules`), or outside its
114
+ * worktree.
115
+ *
116
+ * @example
117
+ * mayWrite({ role: 'test-writer', may_edit_tests: true, path: 'src/sum.test.ts', config }) // true
118
+ * mayWrite({ role: 'implementer', may_edit_tests: false, path: 'src/sum.test.ts', config }) // false
119
+ * mayWrite({ role: 'implementer', may_edit_tests: true, path: 'src/sum.test.ts', config }) // true: a refactor ticket
120
+ */
121
+ export const mayWrite = ({
122
+ role,
123
+ may_edit_tests,
124
+ path,
125
+ config,
126
+ }: GuardArgs & {
127
+ path: string
128
+ config: EngineConfig
129
+ }): boolean => {
130
+ if (!isWriter(role) || isAbsolute(path)) return false
131
+ const clean = normalize(path).replace(/^\.\//, '')
132
+ if (clean === '.' || clean.startsWith('..')) return false
133
+ if (clean === '.git' || clean.startsWith('.git/')) return false
134
+ if (config.test_setup_files.includes(clean)) return false
135
+ if (isInstallPath(clean)) return false
136
+ const test = isTestFile({ path: clean, config })
137
+ // The test-writer may also write the repo's feature map, when it has one
138
+ // (#583): the file, or any page of a folder map (#597); and its test
139
+ // helpers (#619).
140
+ if (role === 'test-writer') {
141
+ return (
142
+ may_edit_tests &&
143
+ (test ||
144
+ isFeatureMapPage({ path: clean, config }) ||
145
+ isTestHelper({ path: clean, config }))
146
+ )
147
+ }
148
+ return !test || may_edit_tests
149
+ }
package/engine/index.ts CHANGED
@@ -20,8 +20,10 @@ export {
20
20
  agentOptions,
21
21
  BANNED_ENV,
22
22
  CLAUDE_MODEL,
23
+ checkLaunchModel,
23
24
  checkModel,
24
25
  MAX_TURNS,
26
+ MODEL_NAMES,
25
27
  resultJsonSchema,
26
28
  } from './agents/claude-options'
27
29
  export {
@@ -60,9 +62,13 @@ export {
60
62
  } from './board/paseo-board-link'
61
63
  export {
62
64
  bunTestCommands,
65
+ DecisionModelConfigSchema,
66
+ decisionModelOf,
63
67
  ENGINE_CONFIG_FILE,
64
68
  EngineConfigSchema,
69
+ hasSlowTests,
65
70
  loadEngineConfig,
71
+ slowTestCommands,
66
72
  testCommands,
67
73
  TestResultsSchema,
68
74
  type EngineConfig,
@@ -128,6 +134,7 @@ export {
128
134
  shipFinalReview,
129
135
  } from './core/execute-final-review'
130
136
  export { finalFixerPrompt, lensPrompt } from './core/final-review-text'
137
+ export { evidenceSection } from './core/evidence-text'
131
138
  export { pullRequestText } from './core/pull-request-text'
132
139
  export {
133
140
  DEFAULT_MAX_STEPS,
@@ -192,27 +199,41 @@ export {
192
199
  } from './intake/intake-checks'
193
200
  export * from './intake/intake-schemas'
194
201
  export {
195
- createTypeSafeJev,
196
- JEV_MODEL,
197
- JEV_URL,
198
- type JevClient,
199
- type JevFetch,
200
- type JevReply,
201
- } from './jev/jev-client'
202
+ clefUrl,
203
+ createClefClient,
204
+ type ClefCredentials,
205
+ type DecisionModelClient,
206
+ type DecisionModelFetch,
207
+ type DecisionModelReply,
208
+ } from './jev/clef-client'
209
+ export {
210
+ CLOUDFLARE_ACCOUNT_ID,
211
+ CLOUDFLARE_API_TOKEN,
212
+ CLOUDFLARE_AUTH_TOKEN,
213
+ DECISION_MODEL_KEYS,
214
+ describeDecisionModelCredentials,
215
+ loadDecisionModelCredentials,
216
+ lucaEnvFile,
217
+ missingKeyLines,
218
+ noCredentialsText,
219
+ parseEnvFile,
220
+ type DecisionModelCredentials,
221
+ type DecisionModelKey,
222
+ } from './jev/decision-model-credentials'
202
223
  export {
203
224
  JEV_CANDIDATE_SKILLS,
204
225
  JEV_FAILURE_TEXT_CHARS,
205
226
  JEV_FIXED_MODEL,
206
- JEV_MODEL_OPTIONS,
207
227
  jevAsksAfter,
208
228
  jevAsksBefore,
209
229
  type JevAsk,
210
230
  } from './jev/jev-jobs'
211
231
  export * from './jev/jev-schemas'
212
232
  export {
213
- askJevInShadow,
214
233
  DEFAULT_JEV_TIMEOUT_MS,
234
+ shadowAsker,
215
235
  type JevShadow,
236
+ type ShadowAsker,
216
237
  } from './jev/jev-shadow'
217
238
  export {
218
239
  createJournal,
@@ -0,0 +1,114 @@
1
+ import uniq from 'lodash/uniq'
2
+
3
+ /** The fix intake asks for when a criterion leaves text to guess. */
4
+ export const PIN_TEXT = 'pin the exact text'
5
+
6
+ /** The fix intake asks for when no agent can test a criterion. */
7
+ export const UNTESTABLE_TEXT =
8
+ 'no agent can check this; move it out of the acceptance criteria'
9
+
10
+ /** The fix intake asks for when a criterion describes what stays as it is. */
11
+ export const UNCHANGED_TEXT =
12
+ "describes behaviour that doesn't change, so no test can fail first; move it out of the acceptance criteria (for example into the ticket's notes)"
13
+
14
+ /**
15
+ * Text that leaves an agent to guess, caught anywhere in a criterion, inside
16
+ * quotes and code spans too (#541).
17
+ */
18
+ const PLACEHOLDERS: RegExp[] = [
19
+ /<[^<>\n]+>/g,
20
+ /\be\.g\./gi,
21
+ /\betc\./gi,
22
+ /\bTBD\b/g,
23
+ /\bTODO\b/g,
24
+ /\.\.\.|…/g,
25
+ /(?<![\w'-])[NMXY](?![\w'-])/g,
26
+ ]
27
+
28
+ /** Asks no agent can test, caught outside code spans only (#541). */
29
+ const UNTESTABLE_ASKS: RegExp[] = [
30
+ /\bscreenshots?\b/gi,
31
+ /\bchangesets?\b/gi,
32
+ /\bADR[\s#-]*\d+/g,
33
+ ]
34
+
35
+ /**
36
+ * Phrases for behaviour that doesn't change, caught outside code spans only
37
+ * (#653): no test can fail first on them.
38
+ */
39
+ const UNCHANGED_PHRASES: RegExp[] = [
40
+ /\bunchanged\b/gi,
41
+ /\bstays? the same\b/gi,
42
+ /\bas (?:today|before)\b/gi,
43
+ ]
44
+
45
+ /** Inline code spans: a run of backticks up to the same run. */
46
+ const CODE_SPAN = /(`+)[\s\S]*?\1/g
47
+
48
+ /** Every match of `patterns` in `text`, in pattern order, once each. */
49
+ const found = ({
50
+ text,
51
+ patterns,
52
+ }: {
53
+ text: string
54
+ patterns: RegExp[]
55
+ }): string[] =>
56
+ uniq(
57
+ patterns.flatMap((pattern) =>
58
+ [...text.matchAll(pattern)].map(([match]) => match)
59
+ )
60
+ )
61
+
62
+ /** `text` as a markdown code span, fenced past any backticks inside it. */
63
+ const asCode = (text: string): string => {
64
+ const longest = Math.max(
65
+ 0,
66
+ ...[...text.matchAll(/`+/g)].map(([run]) => run.length)
67
+ )
68
+ const fence = '`'.repeat(longest + 1)
69
+ return longest === 0
70
+ ? `${fence}${text}${fence}`
71
+ : `${fence} ${text} ${fence}`
72
+ }
73
+
74
+ /**
75
+ * Intake's problems with one ticket's acceptance criteria: a placeholder
76
+ * anywhere in a criterion, an untestable ask (a screenshot, a changeset,
77
+ * an ADR number) outside its code spans, or, outside its code spans, words
78
+ * for behaviour that doesn't change ("unchanged", "stays the same", "as
79
+ * today", "as before"), once per criterion. Each problem names the ticket,
80
+ * quotes the criterion, and says what to do. Pure. Empty when all are clean.
81
+ *
82
+ * @example
83
+ * criteriaProblems({ ticket: 12, criteria: [{ id: 'AC1', text: 'Retries N times' }] })
84
+ * // ['Acceptance criterion AC1 of #12 leaves text to guess (`N`): pin the exact text. It reads: `Retries N times`']
85
+ */
86
+ export const criteriaProblems = ({
87
+ ticket,
88
+ criteria,
89
+ }: {
90
+ ticket: number
91
+ criteria: { id: string; text: string }[]
92
+ }): string[] =>
93
+ criteria.flatMap(({ id, text }) => {
94
+ const named = `Acceptance criterion ${id} of #${ticket}`
95
+ const quoted = `It reads: ${asCode(text)}`
96
+ const placeholders = found({ text, patterns: PLACEHOLDERS })
97
+ const prose = text.replace(CODE_SPAN, ' ')
98
+ const asks = found({ text: prose, patterns: UNTESTABLE_ASKS })
99
+ const unchanged =
100
+ found({ text: prose, patterns: UNCHANGED_PHRASES }).length > 0
101
+ return [
102
+ ...(placeholders.length === 0
103
+ ? []
104
+ : [
105
+ `${named} leaves text to guess (${placeholders.map(asCode).join(', ')}): ${PIN_TEXT}. ${quoted}`,
106
+ ]),
107
+ ...(asks.length === 0
108
+ ? []
109
+ : [
110
+ `${named} asks for ${asks.map(asCode).join(', ')}: ${UNTESTABLE_TEXT}. ${quoted}`,
111
+ ]),
112
+ ...(unchanged ? [`${named} ${UNCHANGED_TEXT}. ${quoted}`] : []),
113
+ ]
114
+ })