@markjaquith/agency 2.73.2 → 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 (59) hide show
  1. package/README.md +35 -134
  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 +11 -71
  10. package/src/commands/context.ts +7 -7
  11. package/src/commands/init.test.ts +1 -2
  12. package/src/commands/pr.test.ts +2 -87
  13. package/src/commands/pr.ts +2 -79
  14. package/src/commands/push.test.ts +1 -2
  15. package/src/graph-schema.ts +1 -1
  16. package/src/services/ArchiveBulkService.test.ts +1 -142
  17. package/src/services/ArchiveService.test.ts +1 -172
  18. package/src/services/ArchiveService.ts +0 -26
  19. package/src/services/ContextService.ts +7 -50
  20. package/src/services/DoctorService.ts +8 -24
  21. package/src/services/GraphService.test.ts +1 -0
  22. package/src/services/GraphService.ts +2 -5
  23. package/src/services/IntegrationService.test.ts +4 -0
  24. package/src/services/PhaseService.ts +70 -191
  25. package/src/services/PullRequestService.test.ts +1 -45
  26. package/src/services/PullRequestService.ts +1 -22
  27. package/src/services/PushService.test.ts +29 -295
  28. package/src/services/PushService.ts +10 -239
  29. package/src/services/RepositoryService.test.ts +1 -61
  30. package/src/services/RepositoryService.ts +2 -13
  31. package/src/services/ReviewService.test.ts +1 -84
  32. package/src/services/ReviewService.ts +9 -48
  33. package/src/services/SyncService.test.ts +0 -81
  34. package/src/services/SyncService.ts +12 -39
  35. package/src/services/TaskPhaseService.test.ts +0 -80
  36. package/src/services/TaskService.ts +1 -9
  37. package/src/services/VersionControlService.test.ts +4 -117
  38. package/src/services/VersionControlService.ts +3 -426
  39. package/src/services/WorkbaseService.test.ts +0 -20
  40. package/src/services/WorkbaseService.ts +1 -18
  41. package/src/services/WorktreeService.test.ts +1 -705
  42. package/src/services/WorktreeService.ts +395 -1604
  43. package/src/services/push-validation.ts +0 -7
  44. package/src/test-utils.ts +0 -4
  45. package/src/workbase/AGENTS.md +7 -7
  46. package/src/workbase/checkout-command.test.ts +1 -1
  47. package/src/workbase/checkout-command.ts +1 -1
  48. package/src/workbase/delivery-command.test.ts +4 -35
  49. package/src/workbase/delivery-command.ts +2 -15
  50. package/src/workbase/schemas.test.ts +0 -19
  51. package/src/workbase/schemas.ts +1 -2
  52. package/src/commands/vcs.test.ts +0 -75
  53. package/src/commands/vcs.ts +0 -72
  54. package/src/services/VcsMigrationService.test.ts +0 -348
  55. package/src/services/VcsMigrationService.ts +0 -857
  56. package/src/vcs-status-fast.ts +0 -310
  57. package/src/workbase/version-control.ts +0 -5
  58. package/src/workbase/workspace-command.test.ts +0 -70
  59. package/src/workbase/workspace-command.ts +0 -63
@@ -22,18 +22,6 @@ const git = async (args: string[], cwd?: string) => {
22
22
  }
23
23
  }
24
24
 
25
- const jj = async (args: string[], cwd?: string) => {
26
- const process = Bun.spawn(["jj", ...args], {
27
- cwd,
28
- stdout: "pipe",
29
- stderr: "pipe",
30
- })
31
- await process.exited
32
- if (process.exitCode !== 0) {
33
- throw new Error(await new Response(process.stderr).text())
34
- }
35
- }
36
-
37
25
  describe("SyncService", () => {
38
26
  let root: string
39
27
  let originalPath: string | undefined
@@ -126,75 +114,6 @@ pr: null
126
114
  expect(await Bun.file(join(root, "repos/agency")).exists()).toBe(false)
127
115
  })
128
116
 
129
- test("reconciles jj workspaces through the jj backend", async () => {
130
- if (!Bun.which("jj")) return
131
- const repository = join(root, "repos/agency")
132
- await rm(repository, { recursive: true, force: true })
133
- await jj([
134
- "git",
135
- "clone",
136
- "--no-colocate",
137
- join(root, "source"),
138
- repository,
139
- ])
140
- expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
141
- await Bun.write(
142
- join(root, "agency.json"),
143
- JSON.stringify({ version: 2, vcs: "jj" }),
144
- )
145
- await runTestEffect(
146
- TaskService.pipe(
147
- Effect.flatMap((service) =>
148
- service.create(
149
- {
150
- id: "jj-sync",
151
- ticketUrl: null,
152
- repo: "agency",
153
- branch: "task/jj-sync",
154
- base: "main",
155
- },
156
- root,
157
- ),
158
- ),
159
- ),
160
- )
161
- const capture = join(root, "gh-capture")
162
- process.env.GH_CAPTURE = capture
163
-
164
- const planned = await runTestEffect(
165
- SyncService.pipe(
166
- Effect.flatMap((service) => service.reconcile({ cwd: root })),
167
- ),
168
- )
169
- expect(planned.changes).toContainEqual(
170
- expect.objectContaining({
171
- kind: "materialize-workspace",
172
- target: "task:jj-sync",
173
- status: "planned",
174
- }),
175
- )
176
- const invocation = await Bun.file(capture).text()
177
- expect(invocation).toContain("--repo\n")
178
- expect(invocation).not.toContain("baseRepository")
179
- expect(invocation).toContain("GIT_DIR=")
180
- expect(invocation).toContain(".jj")
181
-
182
- const applied = await runTestEffect(
183
- SyncService.pipe(
184
- Effect.flatMap((service) =>
185
- service.reconcile({ cwd: root, apply: true }),
186
- ),
187
- ),
188
- )
189
- expect(applied.executions[0]?.checkouts[0]).toMatchObject({
190
- exists: true,
191
- registered: true,
192
- branch: "task/jj-sync",
193
- dirty: false,
194
- })
195
- delete process.env.GH_CAPTURE
196
- })
197
-
198
117
  test("observes drift without mutation and applies only safe transitions", async () => {
199
118
  await runTestEffect(
200
119
  TaskService.pipe(
@@ -352,7 +352,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
352
352
  const registered: RegisteredWorktree[] = []
353
353
  for (const item of listed.right) {
354
354
  registered.push({
355
- head: backend.kind === "jj" ? (item.head ?? null) : item.commit,
355
+ head: item.commit,
356
356
  branch: item.branch,
357
357
  path: (yield* fs.exists(item.path))
358
358
  ? yield* fs.realPath(item.path)
@@ -421,9 +421,6 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
421
421
  env: resolved.environment,
422
422
  })
423
423
  } else if (!config.delivery && existing) {
424
- const environment = yield* Effect.either(
425
- backend.gitEnvironment(repositoryPath),
426
- )
427
424
  result = yield* runExternal(
428
425
  [
429
426
  "gh",
@@ -435,13 +432,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
435
432
  ],
436
433
  {
437
434
  cwd: repositoryPath,
438
- env: Either.isRight(environment) ? environment.right : {},
435
+ env: {},
439
436
  },
440
437
  )
441
438
  } else if (!config.delivery) {
442
- const environment = yield* Effect.either(
443
- backend.gitEnvironment(repositoryPath),
444
- )
445
439
  result = yield* runExternal(
446
440
  [
447
441
  "gh",
@@ -458,7 +452,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
458
452
  ],
459
453
  {
460
454
  cwd: repositoryPath,
461
- env: Either.isRight(environment) ? environment.right : {},
455
+ env: {},
462
456
  },
463
457
  )
464
458
  }
@@ -550,34 +544,15 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
550
544
  : (yield* fs.isDirectory(codePath))
551
545
  ? join(yield* fs.realPath(codePath), checkout.repo)
552
546
  : resolve(checkoutPath)
553
- let atPath = registered.find((item) => item.path === expectedPath)
554
- if (backend.kind === "jj" && atPath && "branch" in checkout) {
555
- atPath = { ...atPath, branch: checkout.branch }
556
- }
557
- if (
558
- backend.kind === "jj" &&
559
- atPath &&
560
- exists &&
561
- atPath.head === null
562
- ) {
563
- atPath = {
564
- ...atPath,
565
- head: yield* backend.workspaceHead(checkoutPath),
566
- }
567
- }
547
+ const atPath = registered.find((item) => item.path === expectedPath)
568
548
  const branchRef =
569
- "branch" in checkout
570
- ? backend.kind === "jj"
571
- ? checkout.branch
572
- : `refs/heads/${checkout.branch}`
573
- : null
574
- const branchElsewhere =
575
- branchRef && backend.kind !== "jj"
576
- ? registered.find(
577
- (item) =>
578
- item.branch === branchRef && item.path !== expectedPath,
579
- )
580
- : undefined
549
+ "branch" in checkout ? `refs/heads/${checkout.branch}` : null
550
+ const branchElsewhere = branchRef
551
+ ? registered.find(
552
+ (item) =>
553
+ item.branch === branchRef && item.path !== expectedPath,
554
+ )
555
+ : undefined
581
556
  if ("branch" in checkout && !exists && !branchElsewhere) {
582
557
  const branch = yield* backend.resolveRevision(
583
558
  repositoryPath,
@@ -773,9 +748,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
773
748
  head: checkout.resolvedCommit,
774
749
  branch:
775
750
  checkout.kind === "writable"
776
- ? backend.kind === "jj"
777
- ? checkout.requestedRef
778
- : `refs/heads/${checkout.requestedRef}`
751
+ ? `refs/heads/${checkout.requestedRef}`
779
752
  : null,
780
753
  dirty: false,
781
754
  })
@@ -7,13 +7,6 @@ import { EpicService } from "./EpicService"
7
7
  import { TaskService } from "./TaskService"
8
8
  import { PhaseService } from "./PhaseService"
9
9
  import { PullRequestService } from "./PullRequestService"
10
- import { WorktreeService } from "./WorktreeService"
11
-
12
- const run = async (args: string[], cwd?: string) => {
13
- const child = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" })
14
- const exitCode = await child.exited
15
- if (exitCode !== 0) throw new Error(await new Response(child.stderr).text())
16
- }
17
10
 
18
11
  describe("task and phase services", () => {
19
12
  let root: string
@@ -528,79 +521,6 @@ describe("task and phase services", () => {
528
521
  })
529
522
  })
530
523
 
531
- test("recreates jj workspaces when converting a task to phases", async () => {
532
- if (!Bun.which("jj")) return
533
- const source = join(root, "source")
534
- const repository = join(root, "repos/agency")
535
- await rm(repository, { recursive: true, force: true })
536
- await mkdir(source)
537
- await run(["git", "init", "--initial-branch=main"], source)
538
- await run(["git", "config", "user.email", "test@example.com"], source)
539
- await run(["git", "config", "user.name", "Test"], source)
540
- await Bun.write(join(source, "README.md"), "example\n")
541
- await run(["git", "add", "README.md"], source)
542
- await run(["git", "commit", "-m", "initial"], source)
543
- await run(["git", "clone", source, repository])
544
- await run(["jj", "git", "init", "--colocate", repository])
545
- await Bun.write(
546
- join(root, "agency.json"),
547
- JSON.stringify({ version: 2, vcs: "jj" }),
548
- )
549
- await runTestEffect(
550
- TaskService.pipe(
551
- Effect.flatMap((service) =>
552
- service.create(
553
- {
554
- id: "jj-single",
555
- ticketUrl: null,
556
- repo: "agency",
557
- branch: "task/jj-single",
558
- base: "main",
559
- },
560
- root,
561
- ),
562
- ),
563
- ),
564
- )
565
- const workspace = await runTestEffect(
566
- WorktreeService.pipe(
567
- Effect.flatMap((service) =>
568
- service.materialize("jj-single", undefined, root),
569
- ),
570
- ),
571
- )
572
-
573
- await runTestEffect(
574
- PhaseService.pipe(
575
- Effect.flatMap((service) =>
576
- service.create(
577
- {
578
- taskId: "jj-single",
579
- id: "extra",
580
- firstPhase: "implementation",
581
- repo: "agency",
582
- branch: "task/extra",
583
- base: "main",
584
- },
585
- root,
586
- ),
587
- ),
588
- ),
589
- )
590
-
591
- const moved = join(
592
- root,
593
- "tasks/jj-single/phases/implementation/code/agency",
594
- )
595
- expect(await Bun.file(workspace.codePath).exists()).toBe(false)
596
- expect(await Bun.file(join(moved, "README.md")).text()).toBe("example\n")
597
- const listed = Bun.spawnSync(
598
- ["jj", "-R", repository, "workspace", "list", "-T", 'root ++ "\\n"'],
599
- { stdout: "pipe" },
600
- )
601
- expect(new TextDecoder().decode(listed.stdout)).toContain(moved)
602
- })
603
-
604
524
  test("preserves non-PR completion when converting a task to phases", async () => {
605
525
  await runTestEffect(
606
526
  TaskService.pipe(
@@ -4,7 +4,6 @@ import { readdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { FileSystemService } from "./FileSystemService"
6
6
  import { WorkbaseService, type ValidationReport } from "./WorkbaseService"
7
- import { VersionControlService } from "./VersionControlService"
8
7
  import { EpicService, type EpicRecord } from "./EpicService"
9
8
  import {
10
9
  EntityId,
@@ -231,7 +230,6 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
231
230
  const fs = yield* FileSystemService
232
231
  const workbase = yield* WorkbaseService
233
232
  const epics = yield* EpicService
234
- const versionControl = yield* VersionControlService
235
233
  const root = yield* workbase.discover(startPath)
236
234
  const id = yield* decodeId(input.id)
237
235
  const directory = join(root, "tasks", id)
@@ -375,13 +373,7 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
375
373
  const updated = formatMarkdownDocument(epicData, parsed.body)
376
374
  writes.push({ path: parentEpic.path, content: updated })
377
375
  }
378
- let reviewEnvironment: Record<string, string> = {}
379
- if (input.review) {
380
- const backend = yield* versionControl.forWorkbase(root)
381
- reviewEnvironment = yield* backend.gitEnvironment(
382
- join(root, "repos", input.review.repo),
383
- )
384
- }
376
+ const reviewEnvironment: Record<string, string> = {}
385
377
  yield* runLifecycleTransaction({
386
378
  root,
387
379
  preconditions: [
@@ -1,6 +1,5 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test"
2
2
  import { Effect, Layer } from "effect"
3
- import { mkdir, realpath, stat } from "node:fs/promises"
4
3
  import { join } from "node:path"
5
4
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
5
  import {
@@ -8,13 +7,6 @@ import {
8
7
  VersionControlService,
9
8
  } from "./VersionControlService"
10
9
  import { FileSystemService } from "./FileSystemService"
11
- import { preferredVersionControl } from "../workbase/version-control"
12
-
13
- const run = async (args: string[], cwd?: string) => {
14
- const child = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" })
15
- const exitCode = await child.exited
16
- if (exitCode !== 0) throw new Error(await new Response(child.stderr).text())
17
- }
18
10
 
19
11
  describe("VersionControlService", () => {
20
12
  const roots: string[] = []
@@ -23,11 +15,6 @@ describe("VersionControlService", () => {
23
15
  await Promise.all(roots.splice(0).map(cleanupTempDir))
24
16
  })
25
17
 
26
- test("prefers jj when available and falls back to Git", () => {
27
- expect(preferredVersionControl(() => "/usr/bin/jj")).toBe("jj")
28
- expect(preferredVersionControl(() => null)).toBe("git")
29
- })
30
-
31
18
  test("inspects a Git repository with one subprocess", async () => {
32
19
  const commands: readonly string[][] = []
33
20
  const fileSystem = {
@@ -61,115 +48,15 @@ describe("VersionControlService", () => {
61
48
  })
62
49
  })
63
50
 
64
- test("selects the backend persisted by the workbase", async () => {
65
- for (const kind of ["git", "jj"] as const) {
66
- const root = await createTempDir()
67
- roots.push(root)
68
- await Bun.write(
69
- join(root, "agency.json"),
70
- JSON.stringify({ version: 2, vcs: kind }),
71
- )
72
- const selected = await runTestEffect(
73
- VersionControlService.pipe(
74
- Effect.flatMap((service) => service.forWorkbase(root)),
75
- ),
76
- )
77
- expect(selected.kind).toBe(kind)
78
- }
79
- })
80
-
81
- test("initializes and manages jj workspaces", async () => {
82
- if (!Bun.which("jj")) return
83
- const root = await createTempDir()
84
- roots.push(root)
85
- await Bun.write(
86
- join(root, "agency.json"),
87
- JSON.stringify({ version: 2, vcs: "jj" }),
88
- )
89
- const repository = join(root, "repository")
90
- const workspace = join(root, "workspace")
91
- await mkdir(repository)
92
- await run(["git", "init", "--initial-branch=main"], repository)
93
- await run(["git", "config", "user.email", "test@example.com"], repository)
94
- await run(["git", "config", "user.name", "Test"], repository)
95
- await Bun.write(join(repository, "README.md"), "example\n")
96
- await run(["git", "add", "README.md"], repository)
97
- await run(["git", "commit", "-m", "initial"], repository)
98
-
99
- const backend = await runTestEffect(
100
- VersionControlService.pipe(
101
- Effect.flatMap((service) => service.forWorkbase(root)),
102
- ),
103
- )
104
- await runTestEffect(backend.initializeRepository(repository))
105
- expect((await stat(join(repository, ".jj"))).isDirectory()).toBe(true)
106
-
107
- const revision = await runTestEffect(
108
- backend.resolveRevision(repository, "main"),
109
- )
110
- expect(revision).toMatch(/^[0-9a-f]{40}$/)
111
- await runTestEffect(
112
- backend.createWorkspace({
113
- repositoryPath: repository,
114
- workspacePath: workspace,
115
- workspaceName: "agency-test",
116
- revision: revision!,
117
- branch: "task/test",
118
- }),
119
- )
120
- const canonicalWorkspace = await realpath(workspace)
121
- expect(
122
- (await runTestEffect(backend.listWorkspaces(repository))).some(
123
- (item) =>
124
- item.name === "agency-test" && item.path === canonicalWorkspace,
125
- ),
126
- ).toBe(true)
127
-
128
- await runTestEffect(
129
- backend.removeWorkspace({
130
- repositoryPath: repository,
131
- workspacePath: workspace,
132
- workspaceName: "agency-test",
133
- }),
134
- )
135
- expect(await Bun.file(workspace).exists()).toBe(false)
136
- })
137
-
138
- test("clones non-colocated jj repositories and exposes their backing Git directory", async () => {
139
- if (!Bun.which("jj")) return
51
+ test("selects Git for legacy and explicit Git workbases", async () => {
140
52
  const root = await createTempDir()
141
53
  roots.push(root)
142
- await Bun.write(
143
- join(root, "agency.json"),
144
- JSON.stringify({ version: 2, vcs: "jj" }),
145
- )
146
- const source = join(root, "source")
147
- const repository = join(root, "repository")
148
- await mkdir(source)
149
- await run(["git", "init", "--initial-branch=main"], source)
150
- await run(["git", "config", "user.email", "test@example.com"], source)
151
- await run(["git", "config", "user.name", "Test"], source)
152
- await Bun.write(join(source, "README.md"), "example\n")
153
- await run(["git", "add", "README.md"], source)
154
- await run(["git", "commit", "-m", "initial"], source)
155
-
156
- const backend = await runTestEffect(
54
+ await Bun.write(join(root, "agency.json"), JSON.stringify({ version: 2 }))
55
+ const selected = await runTestEffect(
157
56
  VersionControlService.pipe(
158
57
  Effect.flatMap((service) => service.forWorkbase(root)),
159
58
  ),
160
59
  )
161
- await runTestEffect(backend.cloneRepository(source, repository))
162
-
163
- expect((await stat(join(repository, ".jj"))).isDirectory()).toBe(true)
164
- expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
165
- const environment: Record<string, string> = await runTestEffect(
166
- backend.gitEnvironment(repository),
167
- )
168
- expect(environment.GIT_DIR).toContain(join(repository, ".jj"))
169
- expect((await stat(environment.GIT_DIR!)).isDirectory()).toBe(true)
170
- expect(await runTestEffect(backend.inspectRepository(repository))).toEqual({
171
- kind: "repository",
172
- remote: await realpath(source),
173
- })
60
+ expect(selected.kind).toBe("git")
174
61
  })
175
62
  })