@markjaquith/agency 2.74.0 → 3.0.0

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 (57) hide show
  1. package/README.md +23 -121
  2. package/cli-main.ts +0 -26
  3. package/cli.ts +1 -17
  4. package/package.json +1 -2
  5. package/schemas/agency-graph-v1.schema.json +1 -1
  6. package/src/cli-parser.test.ts +0 -19
  7. package/src/cli-parser.ts +0 -23
  8. package/src/cli.test.ts +3 -3
  9. package/src/commands/context.test.ts +0 -67
  10. package/src/commands/init.test.ts +1 -2
  11. package/src/commands/pr.test.ts +2 -87
  12. package/src/commands/pr.ts +2 -79
  13. package/src/commands/push.test.ts +1 -2
  14. package/src/graph-schema.ts +1 -1
  15. package/src/services/ArchiveBulkService.test.ts +1 -142
  16. package/src/services/ArchiveService.test.ts +1 -172
  17. package/src/services/ArchiveService.ts +0 -26
  18. package/src/services/ContextService.ts +3 -47
  19. package/src/services/DoctorService.ts +8 -24
  20. package/src/services/GraphService.test.ts +1 -0
  21. package/src/services/GraphService.ts +2 -5
  22. package/src/services/PhaseService.ts +70 -191
  23. package/src/services/PullRequestService.test.ts +1 -45
  24. package/src/services/PullRequestService.ts +1 -22
  25. package/src/services/PushService.test.ts +29 -295
  26. package/src/services/PushService.ts +10 -239
  27. package/src/services/RepositoryService.test.ts +1 -61
  28. package/src/services/RepositoryService.ts +2 -13
  29. package/src/services/ReviewService.test.ts +1 -84
  30. package/src/services/ReviewService.ts +9 -48
  31. package/src/services/SyncService.test.ts +0 -81
  32. package/src/services/SyncService.ts +12 -39
  33. package/src/services/TaskPhaseService.test.ts +0 -80
  34. package/src/services/TaskService.ts +1 -9
  35. package/src/services/VersionControlService.test.ts +4 -117
  36. package/src/services/VersionControlService.ts +3 -426
  37. package/src/services/WorkbaseService.test.ts +0 -20
  38. package/src/services/WorkbaseService.ts +1 -18
  39. package/src/services/WorktreeService.test.ts +1 -705
  40. package/src/services/WorktreeService.ts +395 -1604
  41. package/src/services/push-validation.ts +0 -7
  42. package/src/test-utils.ts +0 -4
  43. package/src/workbase/AGENTS.md +4 -5
  44. package/src/workbase/checkout-command.test.ts +1 -1
  45. package/src/workbase/checkout-command.ts +1 -1
  46. package/src/workbase/delivery-command.test.ts +4 -35
  47. package/src/workbase/delivery-command.ts +2 -15
  48. package/src/workbase/schemas.test.ts +0 -19
  49. package/src/workbase/schemas.ts +1 -2
  50. package/src/commands/vcs.test.ts +0 -75
  51. package/src/commands/vcs.ts +0 -72
  52. package/src/services/VcsMigrationService.test.ts +0 -348
  53. package/src/services/VcsMigrationService.ts +0 -857
  54. package/src/vcs-status-fast.ts +0 -310
  55. package/src/workbase/version-control.ts +0 -5
  56. package/src/workbase/workspace-command.test.ts +0 -70
  57. package/src/workbase/workspace-command.ts +0 -63
@@ -19,14 +19,13 @@ interface CommandResult {
19
19
  type CommitMetadata = PushCommitMetadata
20
20
 
21
21
  interface PushResult {
22
- readonly vcs: "git" | "jj"
22
+ readonly vcs: "git"
23
23
  readonly taskId: string
24
24
  readonly phaseId?: string
25
25
  readonly branch: string
26
26
  readonly base: string
27
27
  readonly remote: string
28
28
  readonly tip: string
29
- readonly changeId?: string
30
29
  }
31
30
 
32
31
  type PushStage = "context" | "fetch" | "inspect" | "validate" | "publish"
@@ -234,224 +233,6 @@ const publishGit = (
234
233
  return { tip }
235
234
  })
236
235
 
237
- const jjTemplate =
238
- '"{\\"commitId\\":" ++ json(commit_id) ++ ",\\"changeId\\":" ++ json(change_id) ++ ",\\"description\\":" ++ json(description) ++ ",\\"empty\\":" ++ json(empty) ++ ",\\"authorName\\":" ++ json(author.name()) ++ ",\\"authorEmail\\":" ++ json(author.email()) ++ ",\\"conflict\\":" ++ json(conflict) ++ ",\\"parents\\":" ++ json(parents.map(|parent| parent.commit_id())) ++ "}\\n"'
239
-
240
- const jjExact = (value: string) =>
241
- value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')
242
-
243
- const jj = (
244
- fs: FileSystemService,
245
- cwd: string,
246
- args: readonly string[],
247
- label: string,
248
- ) => requireCommand(fs, ["jj", "--no-pager", ...args], cwd, label)
249
-
250
- const jjCommits = (fs: FileSystemService, cwd: string, revision: string) =>
251
- jj(
252
- fs,
253
- cwd,
254
- ["log", "--no-graph", "-r", revision, "-T", jjTemplate],
255
- "Failed to inspect jj changes",
256
- ).pipe(
257
- Effect.map((result) =>
258
- result.stdout
259
- .split("\n")
260
- .filter(Boolean)
261
- .map((line) => JSON.parse(line) as CommitMetadata),
262
- ),
263
- )
264
-
265
- const jjRevision = (fs: FileSystemService, cwd: string, revision: string) =>
266
- jjCommits(fs, cwd, revision).pipe(
267
- Effect.map((commits) => {
268
- if (commits.length === 0) return null
269
- if (commits.length === 1) return commits[0]!
270
- throw new PushError({
271
- message: `Revision '${revision}' resolves to multiple jj commits`,
272
- })
273
- }),
274
- )
275
-
276
- const optionalJjRevision = (
277
- fs: FileSystemService,
278
- cwd: string,
279
- revision: string,
280
- ) =>
281
- jjCommits(fs, cwd, revision).pipe(
282
- Effect.catchTag("PushError", () => Effect.succeed([])),
283
- Effect.map((commits) => {
284
- if (commits.length === 0) return null
285
- if (commits.length === 1) return commits[0]!
286
- throw new PushError({
287
- message: `Bookmark '${revision}' is conflicted or resolves to multiple commits`,
288
- })
289
- }),
290
- )
291
-
292
- const jjAncestor = (
293
- fs: FileSystemService,
294
- cwd: string,
295
- ancestor: string,
296
- descendant: string,
297
- ) =>
298
- jjCommits(fs, cwd, `${ancestor} & ::${descendant}`).pipe(
299
- Effect.map((commits) => commits.length === 1),
300
- )
301
-
302
- const validateJjCommits = (
303
- commits: readonly CommitMetadata[],
304
- base: string,
305
- ) => {
306
- if (commits.length === 0) {
307
- throw new PushError({
308
- message: `No changes to publish after base '${base}'`,
309
- })
310
- }
311
- const issues: string[] = []
312
- for (const commit of commits) {
313
- const id = commit.changeId!
314
- if (commit.conflict) {
315
- issues.push(`Change ${id} contains conflicts. Run: jj resolve -r ${id}`)
316
- }
317
- if (!commit.description.trim()) {
318
- issues.push(`Change ${id} has no description. Run: jj describe -r ${id}`)
319
- }
320
- if (!commit.authorName.trim() || !validEmail(commit.authorEmail.trim())) {
321
- issues.push(
322
- `Change ${id} has an invalid author. Run: jj metaedit -r ${id} --author 'Name <email>'`,
323
- )
324
- }
325
- }
326
- if (issues.length > 0) throw new PushError({ message: issues.join("\n") })
327
- }
328
-
329
- const publishJj = (
330
- fs: FileSystemService,
331
- checkout: string,
332
- remote: string,
333
- branch: string,
334
- base: string,
335
- onProgress?: (stage: PushStage) => void,
336
- ) =>
337
- Effect.gen(function* () {
338
- onProgress?.("fetch")
339
- yield* jj(
340
- fs,
341
- checkout,
342
- ["git", "fetch", "--remote", remote],
343
- `Failed to fetch remote '${remote}'`,
344
- )
345
- onProgress?.("inspect")
346
- const workingCopy = yield* jjRevision(fs, checkout, "@")
347
- if (!workingCopy) {
348
- return yield* new PushError({
349
- message: "jj working copy commit was not found",
350
- })
351
- }
352
- const canonicalPostCommit =
353
- workingCopy.empty &&
354
- !workingCopy.description.trim() &&
355
- workingCopy.parents.length === 1
356
- const tip = canonicalPostCommit
357
- ? yield* jjRevision(fs, checkout, "@-")
358
- : workingCopy
359
- if (!tip) {
360
- return yield* new PushError({
361
- message: "jj publication tip was not found",
362
- })
363
- }
364
-
365
- const baseBookmark = `${base}@${remote}`
366
- const [baseRevision, localBookmark, remoteBookmark] = yield* Effect.all(
367
- [
368
- optionalJjRevision(
369
- fs,
370
- checkout,
371
- `remote_bookmarks(exact:"${jjExact(base)}", exact:"${jjExact(remote)}")`,
372
- ),
373
- optionalJjRevision(
374
- fs,
375
- checkout,
376
- `bookmarks(exact:"${jjExact(branch)}")`,
377
- ),
378
- optionalJjRevision(
379
- fs,
380
- checkout,
381
- `remote_bookmarks(exact:"${jjExact(branch)}", exact:"${jjExact(remote)}")`,
382
- ),
383
- ],
384
- { concurrency: "unbounded" },
385
- )
386
- if (!baseRevision) {
387
- return yield* new PushError({
388
- message: `Declared base '${base}' was not found on remote '${remote}'`,
389
- })
390
- }
391
- if (
392
- !(yield* jjAncestor(fs, checkout, baseRevision.commitId, tip.commitId))
393
- ) {
394
- return yield* new PushError({
395
- message: `Declared base '${base}' (${baseRevision.commitId}) is not an ancestor of jj tip ${tip.changeId} (${tip.commitId}). Rebase the stack with: jj rebase -s 'roots(${baseBookmark}..@)' -d ${baseBookmark}`,
396
- })
397
- }
398
-
399
- onProgress?.("validate")
400
- const outgoing = yield* jjCommits(
401
- fs,
402
- checkout,
403
- `${baseRevision.commitId}..${tip.commitId}`,
404
- )
405
- yield* Effect.try({
406
- try: () => validateJjCommits(outgoing, base),
407
- catch: (cause) => cause as PushError,
408
- })
409
-
410
- const [localBookmarkIsAncestor, remoteBookmarkIsAncestor] =
411
- yield* Effect.all(
412
- [
413
- localBookmark
414
- ? jjAncestor(fs, checkout, localBookmark.commitId, tip.commitId)
415
- : Effect.succeed(true),
416
- remoteBookmark
417
- ? jjAncestor(fs, checkout, remoteBookmark.commitId, tip.commitId)
418
- : Effect.succeed(true),
419
- ],
420
- { concurrency: "unbounded" },
421
- )
422
- if (!localBookmarkIsAncestor) {
423
- return yield* new PushError({
424
- message: `Local bookmark '${branch}' is not an ancestor of jj tip ${tip.changeId}; refusing to move it`,
425
- })
426
- }
427
- if (!remoteBookmarkIsAncestor) {
428
- return yield* new PushError({
429
- message: `Remote bookmark '${branch}@${remote}' is not an ancestor of jj tip ${tip.changeId}; refusing a non-fast-forward update`,
430
- })
431
- }
432
-
433
- onProgress?.("publish")
434
- yield* jj(
435
- fs,
436
- checkout,
437
- ["bookmark", "set", branch, "-r", tip.commitId],
438
- `Failed to set declared bookmark '${branch}'`,
439
- )
440
- yield* jj(
441
- fs,
442
- checkout,
443
- ["git", "push", "--remote", remote, "--bookmark", branch],
444
- `Failed to push declared bookmark '${branch}'`,
445
- )
446
- yield* jj(
447
- fs,
448
- checkout,
449
- ["bookmark", "track", `${branch}@${remote}`],
450
- `Failed to track remote bookmark '${branch}@${remote}'`,
451
- )
452
- return { tip: tip.commitId, changeId: tip.changeId }
453
- })
454
-
455
236
  export class PushService extends Effect.Service<PushService>()("PushService", {
456
237
  sync: () => ({
457
238
  publish: (startPath: string = process.cwd(), options: PushOptions = {}) =>
@@ -534,26 +315,16 @@ export class PushService extends Effect.Service<PushService>()("PushService", {
534
315
  const checkout = context.authority.writable.checkoutPath
535
316
  const { config } = yield* workbase.loadConfig(context.workbase.root)
536
317
  const remote = config.delivery?.remote ?? "origin"
537
- const published =
538
- context.workbase.vcs === "jj"
539
- ? yield* publishJj(
540
- fs,
541
- checkout,
542
- remote,
543
- execution.branch,
544
- execution.base,
545
- options.onProgress,
546
- )
547
- : yield* publishGit(
548
- fs,
549
- checkout,
550
- remote,
551
- execution.branch,
552
- execution.base,
553
- options.onProgress,
554
- )
318
+ const published = yield* publishGit(
319
+ fs,
320
+ checkout,
321
+ remote,
322
+ execution.branch,
323
+ execution.base,
324
+ options.onProgress,
325
+ )
555
326
  return {
556
- vcs: context.workbase.vcs,
327
+ vcs: "git",
557
328
  taskId,
558
329
  ...(phaseId ? { phaseId } : {}),
559
330
  branch: execution.branch,
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { Effect } from "effect"
3
- import { mkdir, realpath, stat } from "node:fs/promises"
3
+ import { mkdir, realpath } from "node:fs/promises"
4
4
  import { dirname, join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
6
  import { RepositoryService } from "./RepositoryService"
@@ -110,41 +110,6 @@ describe("RepositoryService", () => {
110
110
  ])
111
111
  })
112
112
 
113
- test("initializes a managed clone with jj for a jj workbase", async () => {
114
- if (!Bun.which("jj")) return
115
- await Bun.write(
116
- join(root, "agency.json"),
117
- JSON.stringify({ version: 2, vcs: "jj" }),
118
- )
119
- const source = join(root, "source")
120
- await runGit(["init", "--initial-branch=main", source])
121
- await runGit(["-C", source, "config", "user.email", "test@example.com"])
122
- await runGit(["-C", source, "config", "user.name", "Test"])
123
- await Bun.write(join(source, "README.md"), "example\n")
124
- await runGit(["-C", source, "add", "README.md"])
125
- await runGit(["-C", source, "commit", "-m", "initial"])
126
- await setPortableOrigin(source, "jj-agency")
127
-
128
- const destination = await runTestEffect(
129
- RepositoryService.pipe(
130
- Effect.flatMap((service) => service.add("agency", source, root)),
131
- ),
132
- )
133
- expect((await stat(join(destination, ".jj"))).isDirectory()).toBe(true)
134
- expect(await Bun.file(join(destination, ".git")).exists()).toBe(false)
135
- expect(
136
- await runTestEffect(
137
- RepositoryService.pipe(
138
- Effect.flatMap((service) => service.show("agency", root)),
139
- ),
140
- ),
141
- ).toMatchObject({
142
- kind: "repository",
143
- remote: portableRemote("jj-agency"),
144
- states: ["declared", "materialized"],
145
- })
146
- })
147
-
148
113
  test("links an existing repository", async () => {
149
114
  const target = join(root, "linked-repository")
150
115
  await mkdir(target, { recursive: true })
@@ -380,31 +345,6 @@ status: working
380
345
  ).not.toContain(linkedWorktree)
381
346
  })
382
347
 
383
- test("refuses to materialize linked aliases in jj workbases", async () => {
384
- if (!Bun.which("jj")) return
385
- await Bun.write(
386
- join(root, "agency.json"),
387
- JSON.stringify({ version: 2, vcs: "jj" }),
388
- )
389
- const target = join(root, "linked-jj-repository")
390
- await mkdir(target, { recursive: true })
391
- await runGit(["init", "--initial-branch=main", target])
392
- await setPortableOrigin(target, "linked-jj")
393
- await runTestEffect(
394
- RepositoryService.pipe(
395
- Effect.flatMap((service) => service.link("linked", target, root)),
396
- ),
397
- )
398
-
399
- await expect(
400
- runTestEffect(
401
- RepositoryService.pipe(
402
- Effect.flatMap((service) => service.materialize("linked", root)),
403
- ),
404
- ),
405
- ).rejects.toThrow("only supported for Git workbases")
406
- })
407
-
408
348
  test("rejects invalid and duplicate aliases", async () => {
409
349
  const source = join(root, "source.git")
410
350
  await runGit(["init", "--bare", "--initial-branch=main", source])
@@ -471,15 +471,9 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
471
471
  })
472
472
  }
473
473
  const inspection = yield* backend.inspectRepository(resolvedTarget)
474
- if (!inspection && backend.kind === "git") {
474
+ if (!inspection) {
475
475
  return yield* new RepositoryError({
476
- message: `Path is not a ${backend.kind} repository: ${resolvedTarget}`,
477
- })
478
- }
479
- yield* backend.initializeRepository(resolvedTarget)
480
- if (!(yield* backend.inspectRepository(resolvedTarget))) {
481
- return yield* new RepositoryError({
482
- message: `Path is not a ${backend.kind} repository: ${resolvedTarget}`,
476
+ message: `Path is not a Git repository: ${resolvedTarget}`,
483
477
  })
484
478
  }
485
479
  const declaredRemote =
@@ -551,11 +545,6 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
551
545
 
552
546
  const state = yield* configState(startPath)
553
547
  const backend = yield* versionControl.forWorkbase(state.root)
554
- if (backend.kind !== "git") {
555
- return yield* new RepositoryError({
556
- message: `Repository alias materialization is only supported for Git workbases`,
557
- })
558
- }
559
548
 
560
549
  const source = yield* fs.realPath(repository.path)
561
550
  const registered = yield* backend.listWorkspaces(repository.path)
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { Effect } from "effect"
3
- import { mkdir, rm } from "node:fs/promises"
3
+ import { mkdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
6
  import { TaskService } from "./TaskService"
@@ -13,7 +13,6 @@ import { PhaseService } from "./PhaseService"
13
13
  import { ArchiveService } from "./ArchiveService"
14
14
  import { ClaimService } from "./ClaimService"
15
15
  import { SyncService } from "./SyncService"
16
- import { DoctorService } from "./DoctorService"
17
16
  import { task as taskCommand } from "../commands/task"
18
17
 
19
18
  const git = async (args: string[], cwd?: string) => {
@@ -27,16 +26,6 @@ const git = async (args: string[], cwd?: string) => {
27
26
  throw new Error(await new Response(child.stderr).text())
28
27
  }
29
28
 
30
- const jj = async (args: string[]) => {
31
- const child = Bun.spawn(["jj", ...args], {
32
- stdout: "pipe",
33
- stderr: "pipe",
34
- })
35
- await child.exited
36
- if (child.exitCode !== 0)
37
- throw new Error(await new Response(child.stderr).text())
38
- }
39
-
40
29
  describe("ReviewService", () => {
41
30
  let root: string
42
31
  let source: string
@@ -162,78 +151,6 @@ describe("ReviewService", () => {
162
151
  expect(new TextDecoder().decode(oldPins.stdout).trim()).toBe("")
163
152
  })
164
153
 
165
- test("imports jj reviews for creation, diagnostics, materialization, and refresh", async () => {
166
- if (!Bun.which("jj")) return
167
- const repository = join(root, "repos/agency")
168
- await rm(repository, { recursive: true, force: true })
169
- await git(["switch", "-c", "jj-review"], source)
170
- await Bun.write(join(source, "README.md"), "jj review one\n")
171
- await git(["commit", "-am", "jj review one"], source)
172
- await jj(["git", "clone", "--no-colocate", source, repository])
173
- expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
174
- await Bun.write(
175
- join(root, "agency.json"),
176
- JSON.stringify({ version: 2, vcs: "jj" }),
177
- )
178
-
179
- await runTestEffect(
180
- taskCommand({
181
- subcommand: "create",
182
- args: ["jj-review"],
183
- review: "agency",
184
- ref: "jj-review",
185
- cwd: root,
186
- silent: true,
187
- }),
188
- )
189
- const created = await runTestEffect(
190
- TaskService.pipe(
191
- Effect.flatMap((service) => service.show("jj-review", root)),
192
- ),
193
- )
194
- const original = "review" in created.data ? created.data.review.commit : ""
195
-
196
- const context = await runTestEffect(
197
- ContextService.pipe(
198
- Effect.flatMap((service) =>
199
- service.get({ target: "jj-review", cwd: root }),
200
- ),
201
- ),
202
- )
203
- expect(context.workspace!.warnings).toEqual([])
204
- expect(context.review?.checkout?.resolvedCommit).toBe(original)
205
-
206
- const doctor = await runTestEffect(
207
- DoctorService.pipe(Effect.flatMap((service) => service.inspect(root))),
208
- )
209
- expect(
210
- doctor.checks.find((check) => check.id === `ref.agency.${original}`),
211
- ).toMatchObject({ status: "pass" })
212
-
213
- const workspace = await runTestEffect(
214
- WorktreeService.pipe(
215
- Effect.flatMap((service) =>
216
- service.materialize("jj-review", undefined, root),
217
- ),
218
- ),
219
- )
220
- expect(
221
- await Bun.file(join(workspace.reviewPath!, "README.md")).text(),
222
- ).toBe("jj review one\n")
223
-
224
- await Bun.write(join(source, "README.md"), "jj review two\n")
225
- await git(["commit", "-am", "jj review two"], source)
226
- const refreshed = await runTestEffect(
227
- ReviewService.pipe(
228
- Effect.flatMap((service) => service.refresh("jj-review", root)),
229
- ),
230
- )
231
- expect(refreshed.changed).toBe(true)
232
- expect(
233
- await Bun.file(join(workspace.reviewPath!, "README.md")).text(),
234
- ).toBe("jj review two\n")
235
- })
236
-
237
154
  test("rejects delivery and phase operations even when forced", async () => {
238
155
  await createReview()
239
156
  await expect(
@@ -1,4 +1,4 @@
1
- import { Data, Effect, Either, Layer } from "effect"
1
+ import { Data, Effect, Layer } from "effect"
2
2
  import { randomUUID } from "node:crypto"
3
3
  import { lstat, mkdir } from "node:fs/promises"
4
4
  import { dirname } from "node:path"
@@ -13,9 +13,7 @@ import {
13
13
  } from "./WorktreeService"
14
14
  import {
15
15
  GitVersionControlService,
16
- JjVersionControlService,
17
16
  VersionControlService,
18
- type VersionControlBackend,
19
17
  } from "./VersionControlService"
20
18
  import { withWorktreeLocks } from "./WorktreeLock"
21
19
  import {
@@ -70,7 +68,6 @@ const WorktreeLayer = Layer.mergeAll(
70
68
  FileSystemService.Default,
71
69
  WorkbaseService.Default,
72
70
  GitVersionControlService.Default,
73
- JjVersionControlService.Default,
74
71
  VersionControlService.Default,
75
72
  TaskService.Default,
76
73
  PhaseService.Default,
@@ -91,22 +88,6 @@ const restoreSnapshots = async (
91
88
  continue
92
89
  } catch {}
93
90
  await mkdir(dirname(snapshot.path), { recursive: true })
94
- if (snapshot.vcs === "jj") {
95
- await runGit([
96
- "jj",
97
- "-R",
98
- snapshot.repositoryPath,
99
- "workspace",
100
- "add",
101
- "--name",
102
- snapshot.workspaceName!,
103
- "-r",
104
- snapshot.head,
105
- snapshot.path,
106
- ])
107
- await runGit(["jj", "-R", snapshot.path, "edit", snapshot.head])
108
- continue
109
- }
110
91
  await runGit(
111
92
  snapshot.branch
112
93
  ? [
@@ -172,14 +153,9 @@ const normalizeBranch = (input: string) =>
172
153
  return `refs/heads/${name}`
173
154
  })
174
155
 
175
- const fetchCommit = (
176
- repoPath: string,
177
- sourceRef: string,
178
- backend: VersionControlBackend,
179
- ) =>
156
+ const fetchCommit = (repoPath: string, sourceRef: string) =>
180
157
  Effect.gen(function* () {
181
158
  const fs = yield* FileSystemService
182
- const environment = yield* backend.gitEnvironment(repoPath)
183
159
  const temporaryRef = `refs/agency/review-fetch/${process.pid}-${randomUUID()}`
184
160
  const fetched = yield* fs.runCommand(
185
161
  [
@@ -191,12 +167,12 @@ const fetchCommit = (
191
167
  "origin",
192
168
  `+${sourceRef}:${temporaryRef}`,
193
169
  ],
194
- { captureOutput: true, env: environment },
170
+ { captureOutput: true },
195
171
  )
196
172
  if (fetched.exitCode !== 0) {
197
173
  const cleanup = yield* fs.runCommand(
198
174
  ["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
199
- { captureOutput: true, env: environment },
175
+ { captureOutput: true },
200
176
  )
201
177
  return yield* new ReviewError({
202
178
  message: `Review source '${sourceRef}' could not be fetched: ${fetched.stderr.trim()}${cleanup.exitCode === 0 ? "" : `; temporary ref cleanup failed: ${cleanup.stderr.trim()}`}`,
@@ -211,34 +187,27 @@ const fetchCommit = (
211
187
  "--verify",
212
188
  `${temporaryRef}^{commit}`,
213
189
  ],
214
- { captureOutput: true, env: environment },
190
+ { captureOutput: true },
215
191
  )
216
192
  const commit = resolved.stdout.trim()
217
193
  if (resolved.exitCode !== 0 || !/^[a-f0-9]{40}$/.test(commit)) {
218
194
  yield* fs.runCommand(
219
195
  ["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
220
- { captureOutput: true, env: environment },
196
+ { captureOutput: true },
221
197
  )
222
198
  return yield* new ReviewError({
223
199
  message: `Review source '${sourceRef}' did not resolve to a commit`,
224
200
  })
225
201
  }
226
- const imported = yield* Effect.either(backend.importGitRefs(repoPath))
227
202
  const cleanup = yield* fs.runCommand(
228
203
  ["git", "-C", repoPath, "update-ref", "-d", temporaryRef],
229
- { captureOutput: true, env: environment },
204
+ { captureOutput: true },
230
205
  )
231
206
  if (cleanup.exitCode !== 0) {
232
207
  return yield* new ReviewError({
233
208
  message: `Failed to remove temporary review fetch ref: ${cleanup.stderr.trim()}`,
234
209
  })
235
210
  }
236
- if (Either.isLeft(imported)) {
237
- return yield* new ReviewError({
238
- message: `Review source '${sourceRef}' was fetched but could not be imported into ${backend.kind}`,
239
- cause: imported.left,
240
- })
241
- }
242
211
  return commit
243
212
  })
244
213
 
@@ -253,8 +222,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
253
222
  ) =>
254
223
  Effect.gen(function* () {
255
224
  const repositories = yield* RepositoryService
256
- const versionControl = yield* VersionControlService
257
- const backend = yield* versionControl.forWorkbase(startPath)
258
225
  const repository = yield* repositories.show(repo, startPath)
259
226
  if (!repository.remote || repository.states.includes("missing")) {
260
227
  return yield* new ReviewError({
@@ -303,7 +270,7 @@ export class ReviewService extends Effect.Service<ReviewService>()(
303
270
  message: "Exactly one review source is required",
304
271
  })
305
272
  }
306
- const commit = yield* fetchCommit(repository.path, sourceRef, backend)
273
+ const commit = yield* fetchCommit(repository.path, sourceRef)
307
274
  return {
308
275
  repo,
309
276
  source,
@@ -371,8 +338,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
371
338
  message: `Repository alias '${task.data.review.repo}' must be materialized with an origin remote`,
372
339
  })
373
340
  }
374
- const versionControl = yield* VersionControlService
375
- const backend = yield* versionControl.forWorkbase(root)
376
341
  const latest = {
377
342
  ...task.data.review,
378
343
  commit: yield* fetchCommit(
@@ -380,7 +345,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
380
345
  task.data.review.source.kind === "pull-request"
381
346
  ? task.data.review.source.fetchRef
382
347
  : task.data.review.source.ref,
383
- backend,
384
348
  ),
385
349
  refreshedAt: new Date().toISOString(),
386
350
  } satisfies ReviewRecord
@@ -389,9 +353,7 @@ export class ReviewService extends Effect.Service<ReviewService>()(
389
353
  { ...task.data, review: latest },
390
354
  parsed.body,
391
355
  )
392
- const gitEnvironment = yield* backend.gitEnvironment(
393
- repository.path,
394
- )
356
+ const gitEnvironment: Record<string, string> = {}
395
357
  const hadCheckout = inspection.checkouts.some(
396
358
  (checkout) => checkout.exists || checkout.registered,
397
359
  )
@@ -405,7 +367,6 @@ export class ReviewService extends Effect.Service<ReviewService>()(
405
367
  worktrees.remove(taskId, undefined, root, {
406
368
  snapshots,
407
369
  lockHeld: true,
408
- persistResume: false,
409
370
  }),
410
371
  ).then(() => undefined),
411
372
  rollback: () => restoreSnapshots(snapshots),