@markjaquith/agency 2.49.0 → 2.51.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 (50) hide show
  1. package/README.md +56 -9
  2. package/cli.ts +56 -1
  3. package/package.json +1 -1
  4. package/schemas/agency-graph-v1.schema.json +142 -38
  5. package/src/cli-parser.test.ts +81 -0
  6. package/src/cli-parser.ts +78 -1
  7. package/src/cli.test.ts +4 -4
  8. package/src/commands/init.test.ts +2 -0
  9. package/src/commands/review.ts +38 -0
  10. package/src/commands/task.ts +27 -5
  11. package/src/commands/vcs.test.ts +75 -0
  12. package/src/commands/vcs.ts +72 -0
  13. package/src/commands/work.test.ts +2 -0
  14. package/src/commands/work.ts +2 -2
  15. package/src/commands/worktree.ts +1 -1
  16. package/src/graph-schema.test.ts +1 -1
  17. package/src/graph-schema.ts +36 -13
  18. package/src/protocol.ts +1 -0
  19. package/src/services/ArchiveService.test.ts +2 -2
  20. package/src/services/ArchiveService.ts +24 -0
  21. package/src/services/ClaimService.ts +3 -2
  22. package/src/services/ContextService.ts +118 -17
  23. package/src/services/DoctorService.ts +58 -7
  24. package/src/services/GraphMutationService.ts +5 -0
  25. package/src/services/GraphService.ts +119 -54
  26. package/src/services/PhaseService.ts +196 -70
  27. package/src/services/PullRequestService.test.ts +2 -2
  28. package/src/services/PullRequestService.ts +37 -33
  29. package/src/services/ReadinessService.ts +7 -3
  30. package/src/services/RepositoryService.test.ts +31 -1
  31. package/src/services/RepositoryService.ts +63 -31
  32. package/src/services/ReviewService.test.ts +472 -0
  33. package/src/services/ReviewService.ts +427 -0
  34. package/src/services/SyncService.test.ts +69 -2
  35. package/src/services/SyncService.ts +161 -90
  36. package/src/services/TaskPhaseService.test.ts +80 -0
  37. package/src/services/TaskService.ts +82 -9
  38. package/src/services/VcsMigrationService.test.ts +211 -0
  39. package/src/services/VcsMigrationService.ts +816 -0
  40. package/src/services/VersionControlService.test.ts +100 -0
  41. package/src/services/VersionControlService.ts +479 -0
  42. package/src/services/WorkbaseService.ts +7 -2
  43. package/src/services/WorktreeService.test.ts +88 -17
  44. package/src/services/WorktreeService.ts +865 -329
  45. package/src/test-utils.ts +12 -0
  46. package/src/work-view.ts +14 -6
  47. package/src/workbase/AGENTS.md +2 -2
  48. package/src/workbase/schemas.test.ts +57 -0
  49. package/src/workbase/schemas.ts +57 -0
  50. package/src/workbase/version-control.ts +5 -0
@@ -0,0 +1,211 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import { lstat, mkdir } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
+ import { TaskService } from "./TaskService"
7
+ import { VcsMigrationService } from "./VcsMigrationService"
8
+ import { WorktreeService } from "./WorktreeService"
9
+
10
+ const run = async (args: string[], cwd?: string) => {
11
+ const child = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" })
12
+ const exitCode = await child.exited
13
+ if (exitCode !== 0) throw new Error(await new Response(child.stderr).text())
14
+ return new Response(child.stdout).text()
15
+ }
16
+
17
+ const exists = async (path: string) => {
18
+ try {
19
+ await lstat(path)
20
+ return true
21
+ } catch {
22
+ return false
23
+ }
24
+ }
25
+
26
+ describe("VcsMigrationService", () => {
27
+ let root: string
28
+
29
+ beforeEach(async () => {
30
+ root = await createTempDir()
31
+ await Bun.write(
32
+ join(root, "agency.json"),
33
+ JSON.stringify({ version: 2, vcs: "git" }),
34
+ )
35
+ const source = join(root, "source")
36
+ await mkdir(source)
37
+ await run(["git", "init", "--initial-branch=main"], source)
38
+ await run(["git", "config", "user.email", "test@example.com"], source)
39
+ await run(["git", "config", "user.name", "Test"], source)
40
+ await Bun.write(join(source, "README.md"), "example\n")
41
+ await run(["git", "add", "README.md"], source)
42
+ await run(["git", "commit", "-m", "initial"], source)
43
+ await mkdir(join(root, "repos"))
44
+ await run(["git", "clone", "--bare", source, join(root, "repos/agency")])
45
+ await run(["git", "clone", "--bare", source, join(root, "repos/effect")])
46
+ await runTestEffect(
47
+ TaskService.pipe(
48
+ Effect.flatMap((service) =>
49
+ service.create(
50
+ {
51
+ id: "example",
52
+ ticketUrl: null,
53
+ repo: "agency",
54
+ repos: [{ repo: "effect", ref: "main" }],
55
+ branch: "task/example",
56
+ base: "main",
57
+ },
58
+ root,
59
+ ),
60
+ ),
61
+ ),
62
+ )
63
+ await runTestEffect(
64
+ WorktreeService.pipe(
65
+ Effect.flatMap((service) =>
66
+ service.materialize("example", undefined, root),
67
+ ),
68
+ ),
69
+ )
70
+ })
71
+
72
+ afterEach(async () => cleanupTempDir(root))
73
+
74
+ test("migrates Git worktrees to jj workspaces and back", async () => {
75
+ if (!Bun.which("jj")) return
76
+ const checkout = join(root, "tasks/example/code/agency")
77
+ const reference = join(root, "tasks/example/code/effect")
78
+ const dryRun = await runTestEffect(
79
+ VcsMigrationService.pipe(
80
+ Effect.flatMap((service) => service.migrate("jj", root)),
81
+ ),
82
+ )
83
+ expect(dryRun).toMatchObject({
84
+ source: "git",
85
+ target: "jj",
86
+ mode: "dry-run",
87
+ workspaceCount: 2,
88
+ })
89
+ expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("git")
90
+
91
+ const migrated = await runTestEffect(
92
+ VcsMigrationService.pipe(
93
+ Effect.flatMap((service) =>
94
+ service.migrate("jj", root, { apply: true }),
95
+ ),
96
+ ),
97
+ )
98
+ expect(migrated.mode).toBe("apply")
99
+ expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("jj")
100
+ expect(await exists(join(root, "repos/agency/.jj"))).toBe(true)
101
+ expect(await exists(join(root, "repos/effect/.jj"))).toBe(true)
102
+ expect(await exists(join(checkout, ".jj"))).toBe(true)
103
+ expect(await exists(join(reference, ".jj"))).toBe(true)
104
+ expect(
105
+ await run(["jj", "-R", checkout, "log", "--no-graph", "-r", "@-"]),
106
+ ).toContain("initial")
107
+ expect(
108
+ (
109
+ await runTestEffect(
110
+ WorktreeService.pipe(
111
+ Effect.flatMap((service) =>
112
+ service.inspect("example", undefined, root),
113
+ ),
114
+ ),
115
+ )
116
+ ).conflicts,
117
+ ).toEqual([])
118
+
119
+ await runTestEffect(
120
+ VcsMigrationService.pipe(
121
+ Effect.flatMap((service) =>
122
+ service.migrate("git", root, { apply: true }),
123
+ ),
124
+ ),
125
+ )
126
+ expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("git")
127
+ expect(await exists(join(root, "repos/agency/.jj"))).toBe(false)
128
+ expect(await exists(join(root, "repos/effect/.jj"))).toBe(false)
129
+ expect(await exists(join(checkout, ".git"))).toBe(true)
130
+ expect(await exists(join(reference, ".git"))).toBe(true)
131
+ expect(
132
+ (await run(["git", "-C", checkout, "branch", "--show-current"])).trim(),
133
+ ).toBe("task/example")
134
+ expect(
135
+ (
136
+ await run(["git", "-C", reference, "rev-parse", "--abbrev-ref", "HEAD"])
137
+ ).trim(),
138
+ ).toBe("HEAD")
139
+ expect(
140
+ (
141
+ await runTestEffect(
142
+ WorktreeService.pipe(
143
+ Effect.flatMap((service) =>
144
+ service.inspect("example", undefined, root),
145
+ ),
146
+ ),
147
+ )
148
+ ).conflicts,
149
+ ).toEqual([])
150
+ })
151
+
152
+ test("blocks active and dirty workspaces", async () => {
153
+ if (!Bun.which("jj")) return
154
+ await runTestEffect(
155
+ TaskService.pipe(
156
+ Effect.flatMap((service) =>
157
+ service.setStatus("example", "working", root),
158
+ ),
159
+ ),
160
+ )
161
+ await expect(
162
+ runTestEffect(
163
+ VcsMigrationService.pipe(
164
+ Effect.flatMap((service) =>
165
+ service.migrate("jj", root, { apply: true }),
166
+ ),
167
+ ),
168
+ ),
169
+ ).rejects.toThrow("is active")
170
+
171
+ await runTestEffect(
172
+ TaskService.pipe(
173
+ Effect.flatMap((service) => service.setStatus("example", "open", root)),
174
+ ),
175
+ )
176
+ await Bun.write(join(root, "tasks/example/code/agency/DIRTY.md"), "dirty\n")
177
+ await expect(
178
+ runTestEffect(
179
+ VcsMigrationService.pipe(
180
+ Effect.flatMap((service) =>
181
+ service.migrate("jj", root, { apply: true }),
182
+ ),
183
+ ),
184
+ ),
185
+ ).rejects.toThrow("must be clean")
186
+ })
187
+
188
+ test("blocks jj-only heads when migrating to Git", async () => {
189
+ if (!Bun.which("jj")) return
190
+ await runTestEffect(
191
+ VcsMigrationService.pipe(
192
+ Effect.flatMap((service) =>
193
+ service.migrate("jj", root, { apply: true }),
194
+ ),
195
+ ),
196
+ )
197
+ const repository = join(root, "repos/agency")
198
+ await run(["jj", "-R", repository, "new", "main", "-m", "hidden head"])
199
+ await run(["jj", "-R", repository, "new", "main"])
200
+
201
+ await expect(
202
+ runTestEffect(
203
+ VcsMigrationService.pipe(
204
+ Effect.flatMap((service) =>
205
+ service.migrate("git", root, { apply: true }),
206
+ ),
207
+ ),
208
+ ),
209
+ ).rejects.toThrow("jj-only heads")
210
+ })
211
+ })