@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
@@ -1,12 +1,7 @@
1
- import { existsSync, realpathSync } from 'node:fs'
2
- import { mkdtemp, rm } from 'node:fs/promises'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
-
6
- import sortBy from 'lodash/sortBy'
7
- import uniq from 'lodash/uniq'
8
-
9
- import { runCommand } from '../shell/run-command'
1
+ import { commitOps } from './git-commits'
2
+ import { joinOps } from './git-join'
3
+ import { readOps } from './git-read'
4
+ import { createWorktreeOps } from './git-worktrees'
10
5
 
11
6
  /** One changed path in a worktree, from `git status`. */
12
7
  export type FileChange = {
@@ -17,6 +12,9 @@ export type FileChange = {
17
12
  /** A commit the engine made. */
18
13
  export type EngineCommit = { sha: string; files: string[] }
19
14
 
15
+ /** Where a commit is, next to a base commit (see `commitOnBase`). */
16
+ export type CommitPlace = 'on_base' | 'not_on_base' | 'unknown'
17
+
20
18
  /**
21
19
  * Every git side effect of a run. Only the engine calls these; agents never
22
20
  * commit, branch, or push.
@@ -59,6 +57,12 @@ export type GitAdapter = {
59
57
  * included.
60
58
  */
61
59
  changedSince: (args: { cwd: string; from: string }) => Promise<string[]>
60
+ /** The text of `path` at commit `sha`, or `null` if it wasn't there (#581). */
61
+ fileAt: (args: {
62
+ cwd: string
63
+ sha: string
64
+ path: string
65
+ }) => Promise<string | null>
62
66
  /** Paths that differ between two commits. */
63
67
  filesBetween: (args: {
64
68
  cwd: string
@@ -87,6 +91,15 @@ export type GitAdapter = {
87
91
  /** Untracked paths to keep out of the commit. Left out, none. */
88
92
  leave_out?: string[]
89
93
  }) => Promise<EngineCommit>
94
+ /**
95
+ * Commits only `paths` (new or changed), skipping hooks. Every other
96
+ * change at `cwd`, staged or not, stays out of the commit, as it was.
97
+ */
98
+ commitPaths: (args: {
99
+ cwd: string
100
+ message: string
101
+ paths: string[]
102
+ }) => Promise<EngineCommit>
90
103
  /**
91
104
  * Throws away every uncommitted change at `cwd`, new untracked files
92
105
  * included (ignored files such as `node_modules` stay), and returns the
@@ -111,22 +124,33 @@ export type GitAdapter = {
111
124
  }) => Promise<string[]>
112
125
  /**
113
126
  * Replays commits onto the branch checked out at `cwd`, in order. A clash
114
- * is undone and returned as an error.
127
+ * is undone and returned as an error. `lines_changed` is the lines the
128
+ * replay added plus removed, binary files counting 0.
115
129
  */
116
130
  replay: (args: {
117
131
  cwd: string
118
132
  commits: string[]
119
- }) => Promise<{ ok: true; shas: string[] } | { ok: false; error: string }>
133
+ }) => Promise<
134
+ | { ok: true; shas: string[]; lines_changed: number }
135
+ | { ok: false; error: string }
136
+ >
120
137
  /**
121
138
  * Undoes a replay on the branch checked out at `cwd`: resets it hard to
122
- * the commit before `first_sha`. Safe to run twice.
139
+ * the commit before `first_sha`. It never rewinds over other work
140
+ * (#519), so it refuses, with the branch left as it is, when
141
+ * `first_sha` is no commit (or has no parent), is not on the branch, or
142
+ * when any commit from `first_sha` up to the branch's tip is not in
143
+ * `only`. Safe to run twice: with the branch already at the commit
144
+ * before `first_sha`, it undoes nothing.
123
145
  *
124
- * @returns The commits it undid, oldest first.
146
+ * @returns The commits it undid, oldest first, or why it refused.
125
147
  */
126
148
  undoReplay: (args: {
127
149
  cwd: string
128
150
  first_sha: string
129
- }) => Promise<{ undone: string[] }>
151
+ /** The commits it may undo: any other on top of `first_sha` refuses. */
152
+ only: string[]
153
+ }) => Promise<{ ok: true; undone: string[] } | { ok: false; error: string }>
130
154
  /**
131
155
  * Moves a worktree's change onto another commit: resets the worktree at
132
156
  * `cwd` hard to `onto`, then applies the whole diff from `from` to `to`
@@ -150,6 +174,16 @@ export type GitAdapter = {
150
174
  push: (args: { cwd: string; branch: string }) => Promise<void>
151
175
  /** The commit checked out at `cwd`. */
152
176
  head: (args: { cwd: string }) => Promise<string>
177
+ /**
178
+ * Where commit `sha` is: `on_base` if it is `base` or in its history,
179
+ * `not_on_base` if the repo has it but `base` doesn't, `unknown` if the
180
+ * repo has no such commit (or `sha` is no commit id).
181
+ */
182
+ commitOnBase: (args: {
183
+ cwd: string
184
+ sha: string
185
+ base: string
186
+ }) => Promise<CommitPlace>
153
187
  /**
154
188
  * The commit checked out at `cwd` with its whole message (trimmed) and
155
189
  * its files, such as to adopt a commit a crash left out of the journal.
@@ -159,84 +193,6 @@ export type GitAdapter = {
159
193
  }) => Promise<EngineCommit & { message: string }>
160
194
  }
161
195
 
162
- const gitRun = ({ cwd, args }: { cwd: string; args: string[] }) =>
163
- runCommand({ cmd: ['git', ...args], cwd, timeout_ms: 120_000 })
164
-
165
- const gitOk = async ({
166
- cwd,
167
- args,
168
- }: {
169
- cwd: string
170
- args: string[]
171
- }): Promise<string> => {
172
- const result = await gitRun({ cwd, args })
173
- if (result.exit_code !== 0) {
174
- throw new Error(
175
- `git ${args.join(' ')} failed in ${cwd}:\n${result.stderr || result.stdout}`
176
- )
177
- }
178
- return result.stdout
179
- }
180
-
181
- const lines = (text: string): string[] =>
182
- text.split('\n').filter((line) => line.trim() !== '')
183
-
184
- /** Parses `git status --porcelain=v1 -z --untracked-files=all`. */
185
- const parseStatus = ({ text }: { text: string }): FileChange[] => {
186
- const parts = text.split('\0').filter((part) => part.length > 0)
187
- const changes: FileChange[] = []
188
- for (let index = 0; index < parts.length; index += 1) {
189
- const part = parts[index] ?? ''
190
- const [x = ' ', y = ' '] = part
191
- const path = part.slice(3)
192
- if (x === 'R' || x === 'C') index += 1 // the next part is the old path
193
- if (x === 'D' || y === 'D') changes.push({ path, change: 'deleted' })
194
- else if (x === '?' || x === 'A' || x === 'R' || x === 'C')
195
- changes.push({ path, change: 'added' })
196
- else changes.push({ path, change: 'modified' })
197
- }
198
- return sortBy(changes, 'path')
199
- }
200
-
201
- /**
202
- * Stages every change at `cwd` (`git add -A`) but the paths in `leave_out`,
203
- * which git never looks at: each is an exact, literal pathspec to exclude,
204
- * passed through a file so any number of them fit.
205
- */
206
- const stageAll = async ({
207
- cwd,
208
- leave_out,
209
- }: {
210
- cwd: string
211
- leave_out: string[]
212
- }): Promise<void> => {
213
- if (leave_out.length === 0) {
214
- await gitOk({ cwd, args: ['add', '-A'] })
215
- return
216
- }
217
- const folder = await mkdtemp(join(tmpdir(), 'luca-stage-'))
218
- try {
219
- const pathspecs = join(folder, 'pathspecs')
220
- await Bun.write(
221
- pathspecs,
222
- ['.', ...leave_out.map((path) => `:(exclude,literal)${path}`)]
223
- .map((spec) => `${spec}\0`)
224
- .join('')
225
- )
226
- await gitOk({
227
- cwd,
228
- args: [
229
- 'add',
230
- '-A',
231
- `--pathspec-from-file=${pathspecs}`,
232
- '--pathspec-file-nul',
233
- ],
234
- })
235
- } finally {
236
- await rm(folder, { recursive: true, force: true })
237
- }
238
- }
239
-
240
196
  /**
241
197
  * Runs async work one piece at a time, in call order, so git never races
242
198
  * itself on the repo's lock files when tickets build at the same time.
@@ -263,311 +219,11 @@ export const createGitAdapter = ({
263
219
  }: {
264
220
  repo_root: string
265
221
  }): GitAdapter => {
266
- const head = async ({ cwd }: { cwd: string }) =>
267
- (await gitOk({ cwd, args: ['rev-parse', 'HEAD'] })).trim()
268
-
269
- /** The branch git has checked out in a worktree at `path`, if any. */
270
- const worktreeBranchAt = async ({
271
- path,
272
- }: {
273
- path: string
274
- }): Promise<{ branch: string | null } | null> => {
275
- if (!existsSync(path)) return null
276
- const wanted = realpathSync(path)
277
- const listed = await gitOk({
278
- cwd: repo_root,
279
- args: ['worktree', 'list', '--porcelain'],
280
- })
281
- for (const block of listed.split('\n\n')) {
282
- const fields = lines(block)
283
- const at = fields
284
- .find((line) => line.startsWith('worktree '))
285
- ?.slice('worktree '.length)
286
- if (at === undefined || !existsSync(at)) continue
287
- if (realpathSync(at) !== wanted) continue
288
- const ref = fields
289
- .find((line) => line.startsWith('branch '))
290
- ?.slice('branch '.length)
291
- return { branch: ref?.replace(/^refs\/heads\//, '') ?? null }
292
- }
293
- return null
294
- }
295
-
296
- const branchExists = async ({ branch }: { branch: string }) =>
297
- (
298
- await gitRun({
299
- cwd: repo_root,
300
- args: [
301
- 'rev-parse',
302
- '--verify',
303
- '--quiet',
304
- `refs/heads/${branch}`,
305
- ],
306
- })
307
- ).exit_code === 0
308
-
309
- const addWorktree = async ({
310
- branch,
311
- from,
312
- path,
313
- }: {
314
- branch: string
315
- from: string
316
- path: string
317
- }) => {
318
- const known = await worktreeBranchAt({ path })
319
- if (known !== null && known.branch !== branch) {
320
- throw new Error(
321
- `${path} is already a worktree on ${known.branch ?? 'no branch'}, not ${branch}.`
322
- )
323
- }
324
- if (known !== null) {
325
- // Made before a crash: nothing has worked in it since, so finish
326
- // a checkout the crash may have cut off, and adopt it.
327
- const reset = await gitRun({
328
- cwd: path,
329
- args: ['reset', '--quiet', '--hard', 'HEAD'],
330
- })
331
- if (reset.exit_code === 0) {
332
- return { base_sha: await head({ cwd: path }) }
333
- }
334
- await gitRun({
335
- cwd: repo_root,
336
- args: ['worktree', 'remove', '--force', '--force', path],
337
- })
338
- }
339
- // A half-made folder git doesn't know (or no longer does) goes.
340
- await rm(path, { recursive: true, force: true })
341
- await gitOk({ cwd: repo_root, args: ['worktree', 'prune'] })
342
- await gitOk({
343
- cwd: repo_root,
344
- args: (await branchExists({ branch }))
345
- ? ['worktree', 'add', '--quiet', path, branch]
346
- : [
347
- 'worktree',
348
- 'add',
349
- '--quiet',
350
- '--no-track',
351
- '-b',
352
- branch,
353
- path,
354
- from,
355
- ],
356
- })
357
- return { base_sha: await head({ cwd: path }) }
358
- }
359
-
360
- const filesOf = async ({ cwd, sha }: { cwd: string; sha: string }) =>
361
- lines(
362
- await gitOk({
363
- cwd,
364
- args: ['show', '--name-only', '--format=', sha],
365
- })
366
- ).toSorted()
367
-
368
222
  const raw: GitAdapter = {
369
- fetchBase: async ({ base_branch }) => {
370
- const fetched = await gitRun({
371
- cwd: repo_root,
372
- args: [
373
- 'fetch',
374
- '--quiet',
375
- 'origin',
376
- `+refs/heads/${base_branch}:refs/remotes/origin/${base_branch}`,
377
- ],
378
- })
379
- return fetched.exit_code === 0
380
- ? { ok: true }
381
- : {
382
- ok: false,
383
- error: (fetched.stderr || fetched.stdout).trim(),
384
- }
385
- },
386
- createRunBranch: ({ branch, base_branch, path }) =>
387
- addWorktree({ branch, from: `origin/${base_branch}`, path }),
388
- createWorktree: addWorktree,
389
- changes: async ({ cwd }) =>
390
- parseStatus({
391
- text: await gitOk({
392
- cwd,
393
- args: [
394
- 'status',
395
- '--porcelain=v1',
396
- '-z',
397
- '--untracked-files=all',
398
- ],
399
- }),
400
- }),
401
- changedSince: async ({ cwd, from }) => {
402
- const tracked = lines(
403
- await gitOk({ cwd, args: ['diff', '--name-only', from] })
404
- )
405
- const untracked = lines(
406
- await gitOk({
407
- cwd,
408
- args: ['ls-files', '--others', '--exclude-standard'],
409
- })
410
- )
411
- return uniq([...tracked, ...untracked]).toSorted()
412
- },
413
- filesBetween: async ({ cwd, from, to }) =>
414
- lines(
415
- await gitOk({ cwd, args: ['diff', '--name-only', from, to] })
416
- ).toSorted(),
417
- untracked: async ({ cwd }) =>
418
- (
419
- await gitOk({
420
- cwd,
421
- args: ['ls-files', '--others', '--exclude-standard', '-z'],
422
- })
423
- )
424
- .split('\0')
425
- .filter((path) => path !== ''),
426
- listFiles: async ({ cwd }) =>
427
- lines(
428
- await gitOk({
429
- cwd,
430
- args: [
431
- 'ls-files',
432
- '--cached',
433
- '--others',
434
- '--exclude-standard',
435
- ],
436
- })
437
- ),
438
- filesMentioning: async ({ cwd, text }) => {
439
- // git grep exits 1 when nothing matches; that is an empty answer.
440
- const result = await gitRun({
441
- cwd,
442
- args: ['grep', '-l', '-F', '-e', text, '--', '.'],
443
- })
444
- return result.exit_code === 0 ? lines(result.stdout) : []
445
- },
446
- commitAll: async ({ cwd, message, leave_out }) => {
447
- await stageAll({ cwd, leave_out: leave_out ?? [] })
448
- await gitOk({
449
- cwd,
450
- args: ['commit', '--quiet', '--no-verify', '-m', message],
451
- })
452
- const sha = await head({ cwd })
453
- return { sha, files: await filesOf({ cwd, sha }) }
454
- },
455
- discardChanges: async ({ cwd }) => {
456
- await gitOk({ cwd, args: ['reset', '--quiet', '--hard', 'HEAD'] })
457
- await gitOk({ cwd, args: ['clean', '--quiet', '-f', '-d'] })
458
- return { sha: await head({ cwd }) }
459
- },
460
- resetWorktree: async ({ cwd, to }) => {
461
- // A cherry-pick a crash cut off leaves its sequence behind.
462
- await gitRun({ cwd, args: ['cherry-pick', '--quit'] })
463
- await gitOk({ cwd, args: ['reset', '--quiet', '--hard', to] })
464
- await gitOk({ cwd, args: ['clean', '--quiet', '-f', '-d'] })
465
- return { sha: await head({ cwd }) }
466
- },
467
- commitsBetween: async ({ cwd, from, to }) =>
468
- lines(
469
- await gitOk({
470
- cwd,
471
- args: ['rev-list', '--reverse', `${from}..${to}`],
472
- })
473
- ),
474
- replay: async ({ cwd, commits }) => {
475
- const before = await head({ cwd })
476
- const picked = await gitRun({
477
- cwd,
478
- args: ['cherry-pick', ...commits],
479
- })
480
- if (picked.exit_code !== 0) {
481
- await gitRun({ cwd, args: ['cherry-pick', '--abort'] })
482
- return { ok: false, error: picked.stderr || picked.stdout }
483
- }
484
- return {
485
- ok: true,
486
- shas: lines(
487
- await gitOk({
488
- cwd,
489
- args: ['rev-list', '--reverse', `${before}..HEAD`],
490
- })
491
- ),
492
- }
493
- },
494
- undoReplay: async ({ cwd, first_sha }) => {
495
- const before = `${first_sha}^`
496
- const undone = lines(
497
- await gitOk({
498
- cwd,
499
- args: ['rev-list', '--reverse', `${before}..HEAD`],
500
- })
501
- )
502
- await gitOk({ cwd, args: ['reset', '--quiet', '--hard', before] })
503
- return { undone }
504
- },
505
- rebaseWorktree: async ({ cwd, from, to, onto }) => {
506
- const folder = await mkdtemp(join(tmpdir(), 'luca-rebase-'))
507
- try {
508
- const patch = join(folder, 'change.patch')
509
- await gitOk({
510
- cwd,
511
- args: ['diff', '--binary', `--output=${patch}`, from, to],
512
- })
513
- await gitOk({ cwd, args: ['reset', '--quiet', '--hard', onto] })
514
- if ((await Bun.file(patch).size) === 0) return { conflicts: [] }
515
- const applied = await gitRun({
516
- cwd,
517
- args: ['apply', '--3way', patch],
518
- })
519
- const conflicts = lines(
520
- await gitOk({
521
- cwd,
522
- args: ['diff', '--name-only', '--diff-filter=U'],
523
- })
524
- ).toSorted()
525
- if (applied.exit_code !== 0 && conflicts.length === 0) {
526
- throw new Error(
527
- `git apply --3way failed in ${cwd}:\n${applied.stderr || applied.stdout}`
528
- )
529
- }
530
- // Everything becomes plain uncommitted changes, markers kept.
531
- await gitOk({ cwd, args: ['reset', '--quiet'] })
532
- return { conflicts }
533
- } finally {
534
- await rm(folder, { recursive: true, force: true })
535
- }
536
- },
537
- removeWorktree: async ({ path }) => {
538
- if (existsSync(path)) {
539
- // A folder that is no worktree (or not any more) is fine.
540
- await gitRun({
541
- cwd: repo_root,
542
- args: ['worktree', 'remove', '--force', path],
543
- })
544
- }
545
- await gitOk({ cwd: repo_root, args: ['worktree', 'prune'] })
546
- },
547
- push: async ({ cwd, branch }) => {
548
- await gitOk({
549
- cwd,
550
- args: [
551
- 'push',
552
- '--quiet',
553
- 'origin',
554
- `${branch}:refs/heads/${branch}`,
555
- ],
556
- })
557
- },
558
- head,
559
- lastCommit: async ({ cwd }) => {
560
- const sha = await head({ cwd })
561
- const message = await gitOk({
562
- cwd,
563
- args: ['log', '-1', '--format=%B', sha],
564
- })
565
- return {
566
- sha,
567
- message: message.trim(),
568
- files: await filesOf({ cwd, sha }),
569
- }
570
- },
223
+ ...createWorktreeOps({ repo_root }),
224
+ ...readOps,
225
+ ...commitOps,
226
+ ...joinOps,
571
227
  }
572
228
 
573
229
  const inTurn = createQueue()
@@ -581,11 +237,13 @@ export const createGitAdapter = ({
581
237
  createWorktree: serial(raw.createWorktree),
582
238
  changes: serial(raw.changes),
583
239
  changedSince: serial(raw.changedSince),
240
+ fileAt: serial(raw.fileAt),
584
241
  filesBetween: serial(raw.filesBetween),
585
242
  untracked: serial(raw.untracked),
586
243
  listFiles: serial(raw.listFiles),
587
244
  filesMentioning: serial(raw.filesMentioning),
588
245
  commitAll: serial(raw.commitAll),
246
+ commitPaths: serial(raw.commitPaths),
589
247
  discardChanges: serial(raw.discardChanges),
590
248
  resetWorktree: serial(raw.resetWorktree),
591
249
  commitsBetween: serial(raw.commitsBetween),
@@ -595,6 +253,7 @@ export const createGitAdapter = ({
595
253
  removeWorktree: serial(raw.removeWorktree),
596
254
  push: serial(raw.push),
597
255
  head: serial(raw.head),
256
+ commitOnBase: serial(raw.commitOnBase),
598
257
  lastCommit: serial(raw.lastCommit),
599
258
  }
600
259
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The git adapter's commits and clean-ups: stage and commit all or some
3
+ * paths, read the last commit, throw away or reset a worktree's changes, and
4
+ * push. Every call skips hooks: the engine ran the gates.
5
+ */
6
+ import { mkdtemp, rm } from 'node:fs/promises'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+
10
+ import type { GitAdapter } from './git-adapter'
11
+ import { gitOk, gitRun, head, lines } from './git-run'
12
+
13
+ /**
14
+ * Stages every change at `cwd` (`git add -A`) but the paths in `leave_out`,
15
+ * which git never looks at: each is an exact, literal pathspec to exclude,
16
+ * passed through a file so any number of them fit.
17
+ */
18
+ export const stageAll = async ({
19
+ cwd,
20
+ leave_out,
21
+ }: {
22
+ cwd: string
23
+ leave_out: string[]
24
+ }): Promise<void> => {
25
+ if (leave_out.length === 0) {
26
+ await gitOk({ cwd, args: ['add', '-A'] })
27
+ return
28
+ }
29
+ const folder = await mkdtemp(join(tmpdir(), 'luca-stage-'))
30
+ try {
31
+ const pathspecs = join(folder, 'pathspecs')
32
+ await Bun.write(
33
+ pathspecs,
34
+ ['.', ...leave_out.map((path) => `:(exclude,literal)${path}`)]
35
+ .map((spec) => `${spec}\0`)
36
+ .join('')
37
+ )
38
+ await gitOk({
39
+ cwd,
40
+ args: [
41
+ 'add',
42
+ '-A',
43
+ `--pathspec-from-file=${pathspecs}`,
44
+ '--pathspec-file-nul',
45
+ ],
46
+ })
47
+ } finally {
48
+ await rm(folder, { recursive: true, force: true })
49
+ }
50
+ }
51
+
52
+ export const filesOf = async ({ cwd, sha }: { cwd: string; sha: string }) =>
53
+ lines(
54
+ await gitOk({
55
+ cwd,
56
+ args: ['show', '--name-only', '--format=', sha],
57
+ })
58
+ ).toSorted()
59
+
60
+ export const commitOps: Pick<
61
+ GitAdapter,
62
+ | 'commitAll'
63
+ | 'commitPaths'
64
+ | 'discardChanges'
65
+ | 'resetWorktree'
66
+ | 'push'
67
+ | 'lastCommit'
68
+ > = {
69
+ commitAll: async ({ cwd, message, leave_out }) => {
70
+ await stageAll({ cwd, leave_out: leave_out ?? [] })
71
+ await gitOk({
72
+ cwd,
73
+ args: ['commit', '--quiet', '--no-verify', '-m', message],
74
+ })
75
+ const sha = await head({ cwd })
76
+ return { sha, files: await filesOf({ cwd, sha }) }
77
+ },
78
+ commitPaths: async ({ cwd, message, paths }) => {
79
+ const pathspecs = paths.map((path) => `:(literal)${path}`)
80
+ await gitOk({ cwd, args: ['add', '--', ...pathspecs] })
81
+ // With paths, `git commit` commits just them (`--only`).
82
+ await gitOk({
83
+ cwd,
84
+ args: [
85
+ 'commit',
86
+ '--quiet',
87
+ '--no-verify',
88
+ '-m',
89
+ message,
90
+ '--',
91
+ ...pathspecs,
92
+ ],
93
+ })
94
+ const sha = await head({ cwd })
95
+ return { sha, files: await filesOf({ cwd, sha }) }
96
+ },
97
+ discardChanges: async ({ cwd }) => {
98
+ await gitOk({ cwd, args: ['reset', '--quiet', '--hard', 'HEAD'] })
99
+ await gitOk({ cwd, args: ['clean', '--quiet', '-f', '-d'] })
100
+ return { sha: await head({ cwd }) }
101
+ },
102
+ resetWorktree: async ({ cwd, to }) => {
103
+ // A cherry-pick a crash cut off leaves its sequence behind.
104
+ await gitRun({ cwd, args: ['cherry-pick', '--quit'] })
105
+ await gitOk({ cwd, args: ['reset', '--quiet', '--hard', to] })
106
+ await gitOk({ cwd, args: ['clean', '--quiet', '-f', '-d'] })
107
+ return { sha: await head({ cwd }) }
108
+ },
109
+ push: async ({ cwd, branch }) => {
110
+ await gitOk({
111
+ cwd,
112
+ args: [
113
+ 'push',
114
+ '--quiet',
115
+ 'origin',
116
+ `${branch}:refs/heads/${branch}`,
117
+ ],
118
+ })
119
+ },
120
+ lastCommit: async ({ cwd }) => {
121
+ const sha = await head({ cwd })
122
+ const message = await gitOk({
123
+ cwd,
124
+ args: ['log', '-1', '--format=%B', sha],
125
+ })
126
+ return {
127
+ sha,
128
+ message: message.trim(),
129
+ files: await filesOf({ cwd, sha }),
130
+ }
131
+ },
132
+ }