@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
@@ -1,348 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import { Effect } from "effect"
3
- import { lstat, mkdir, rm } from "node:fs/promises"
4
- import { join } from "node:path"
5
- import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
- import { ClaimService } from "./ClaimService"
7
- import { TaskService } from "./TaskService"
8
- import { VcsMigrationService } from "./VcsMigrationService"
9
- import { WorktreeService } from "./WorktreeService"
10
- import { runVcsStatusFast } from "../vcs-status-fast"
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
- return new Response(child.stdout).text()
17
- }
18
-
19
- const exists = async (path: string) => {
20
- try {
21
- await lstat(path)
22
- return true
23
- } catch {
24
- return false
25
- }
26
- }
27
-
28
- describe("VcsMigrationService", () => {
29
- let root: string
30
-
31
- beforeEach(async () => {
32
- root = await createTempDir()
33
- await Bun.write(
34
- join(root, "agency.json"),
35
- JSON.stringify({ version: 2, vcs: "git" }),
36
- )
37
- const source = join(root, "source")
38
- await mkdir(source)
39
- await run(["git", "init", "--initial-branch=main"], source)
40
- await run(["git", "config", "user.email", "test@example.com"], source)
41
- await run(["git", "config", "user.name", "Test"], source)
42
- await Bun.write(join(source, "README.md"), "example\n")
43
- await run(["git", "add", "README.md"], source)
44
- await run(["git", "commit", "-m", "initial"], source)
45
- await mkdir(join(root, "repos"))
46
- await run(["git", "clone", "--bare", source, join(root, "repos/agency")])
47
- await run(["git", "clone", "--bare", source, join(root, "repos/effect")])
48
- await runTestEffect(
49
- TaskService.pipe(
50
- Effect.flatMap((service) =>
51
- service.create(
52
- {
53
- id: "example",
54
- ticketUrl: null,
55
- repo: "agency",
56
- repos: [{ repo: "effect", ref: "main" }],
57
- branch: "task/example",
58
- base: "main",
59
- },
60
- root,
61
- ),
62
- ),
63
- ),
64
- )
65
- await runTestEffect(
66
- WorktreeService.pipe(
67
- Effect.flatMap((service) =>
68
- service.materialize("example", undefined, root),
69
- ),
70
- ),
71
- )
72
- })
73
-
74
- afterEach(async () => cleanupTempDir(root))
75
-
76
- test("reports same-backend status for materialized workspaces", async () => {
77
- const status = await runTestEffect(
78
- VcsMigrationService.pipe(
79
- Effect.flatMap((service) => service.status(root)),
80
- ),
81
- )
82
- expect(status).toMatchObject({
83
- configured: "git",
84
- source: "git",
85
- target: "git",
86
- workspaceCount: 2,
87
- blockers: [],
88
- })
89
- })
90
-
91
- test("fast jj status matches the validated service result", async () => {
92
- if (!Bun.which("jj")) return
93
- await runTestEffect(
94
- VcsMigrationService.pipe(
95
- Effect.flatMap((service) =>
96
- service.migrate("jj", root, { apply: true }),
97
- ),
98
- ),
99
- )
100
- const taskPath = join(root, "tasks/example/TASK.md")
101
- const content = await Bun.file(taskPath).text()
102
- const withoutReference = content.replace(
103
- /\nrepos:\n(?: .+\n)+(?=branch:)/,
104
- "\n",
105
- )
106
- expect(withoutReference).not.toBe(content)
107
- await Bun.write(taskPath, withoutReference)
108
-
109
- const validated = await runTestEffect(
110
- VcsMigrationService.pipe(
111
- Effect.flatMap((service) => service.status(root)),
112
- ),
113
- )
114
- const output: string[] = []
115
- expect(
116
- await runVcsStatusFast(true, root, (line) => output.push(line)),
117
- ).toBe(true)
118
- expect(JSON.parse(output.join("\n")).result).toEqual(validated)
119
- })
120
-
121
- test("migrates Git worktrees to jj workspaces and back", async () => {
122
- if (!Bun.which("jj")) return
123
- const checkout = join(root, "tasks/example/code/agency")
124
- const reference = join(root, "tasks/example/code/effect")
125
- const dryRun = await runTestEffect(
126
- VcsMigrationService.pipe(
127
- Effect.flatMap((service) => service.migrate("jj", root)),
128
- ),
129
- )
130
- expect(dryRun).toMatchObject({
131
- source: "git",
132
- target: "jj",
133
- mode: "dry-run",
134
- workspaceCount: 2,
135
- })
136
- expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("git")
137
-
138
- const migrated = await runTestEffect(
139
- VcsMigrationService.pipe(
140
- Effect.flatMap((service) =>
141
- service.migrate("jj", root, { apply: true }),
142
- ),
143
- ),
144
- )
145
- expect(migrated.mode).toBe("apply")
146
- expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("jj")
147
- expect(await exists(join(root, "repos/agency/.jj"))).toBe(true)
148
- expect(await exists(join(root, "repos/effect/.jj"))).toBe(true)
149
- expect(await exists(join(checkout, ".jj"))).toBe(true)
150
- expect(await exists(join(reference, ".jj"))).toBe(true)
151
- expect(
152
- await run(["jj", "-R", checkout, "log", "--no-graph", "-r", "@-"]),
153
- ).toContain("initial")
154
- expect(
155
- (
156
- await runTestEffect(
157
- WorktreeService.pipe(
158
- Effect.flatMap((service) =>
159
- service.inspect("example", undefined, root),
160
- ),
161
- ),
162
- )
163
- ).conflicts,
164
- ).toEqual([])
165
-
166
- await runTestEffect(
167
- VcsMigrationService.pipe(
168
- Effect.flatMap((service) =>
169
- service.migrate("git", root, { apply: true }),
170
- ),
171
- ),
172
- )
173
- expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("git")
174
- expect(await exists(join(root, "repos/agency/.jj"))).toBe(false)
175
- expect(await exists(join(root, "repos/effect/.jj"))).toBe(false)
176
- expect(await exists(join(checkout, ".git"))).toBe(true)
177
- expect(await exists(join(reference, ".git"))).toBe(true)
178
- expect(
179
- (await run(["git", "-C", checkout, "branch", "--show-current"])).trim(),
180
- ).toBe("task/example")
181
- expect(
182
- (
183
- await run(["git", "-C", reference, "rev-parse", "--abbrev-ref", "HEAD"])
184
- ).trim(),
185
- ).toBe("HEAD")
186
- expect(
187
- (
188
- await runTestEffect(
189
- WorktreeService.pipe(
190
- Effect.flatMap((service) =>
191
- service.inspect("example", undefined, root),
192
- ),
193
- ),
194
- )
195
- ).conflicts,
196
- ).toEqual([])
197
- })
198
-
199
- test("blocks non-colocated jj to Git migration before mutation", async () => {
200
- if (!Bun.which("jj")) return
201
- await runTestEffect(
202
- WorktreeService.pipe(
203
- Effect.flatMap((service) => service.remove("example", undefined, root)),
204
- ),
205
- )
206
- for (const alias of ["agency", "effect"]) {
207
- const repository = join(root, "repos", alias)
208
- await rm(repository, { recursive: true, force: true })
209
- await run([
210
- "jj",
211
- "git",
212
- "clone",
213
- "--no-colocate",
214
- join(root, "source"),
215
- repository,
216
- ])
217
- expect(await exists(join(repository, ".git"))).toBe(false)
218
- }
219
- await Bun.write(
220
- join(root, "agency.json"),
221
- JSON.stringify({ version: 2, vcs: "jj" }),
222
- )
223
-
224
- const planned = await runTestEffect(
225
- VcsMigrationService.pipe(
226
- Effect.flatMap((service) => service.migrate("git", root)),
227
- ),
228
- )
229
- expect(planned.blockers).toEqual(
230
- expect.arrayContaining([
231
- expect.objectContaining({
232
- target: "repository:agency",
233
- message: expect.stringContaining("non-colocated"),
234
- }),
235
- expect.objectContaining({
236
- target: "repository:effect",
237
- message: expect.stringContaining("non-colocated"),
238
- }),
239
- ]),
240
- )
241
- expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("jj")
242
- expect(await exists(join(root, "repos/agency/.jj"))).toBe(true)
243
- await expect(
244
- runTestEffect(
245
- VcsMigrationService.pipe(
246
- Effect.flatMap((service) =>
247
- service.migrate("git", root, { apply: true }),
248
- ),
249
- ),
250
- ),
251
- ).rejects.toThrow("non-colocated")
252
- expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("jj")
253
- expect(await exists(join(root, "repos/agency/.jj"))).toBe(true)
254
- })
255
-
256
- test("allows clean unclaimed working workspaces", async () => {
257
- if (!Bun.which("jj")) return
258
- await runTestEffect(
259
- TaskService.pipe(
260
- Effect.flatMap((service) =>
261
- service.setStatus("example", "working", root),
262
- ),
263
- ),
264
- )
265
- const migrated = await runTestEffect(
266
- VcsMigrationService.pipe(
267
- Effect.flatMap((service) =>
268
- service.migrate("jj", root, { apply: true }),
269
- ),
270
- ),
271
- )
272
- expect(migrated.mode).toBe("apply")
273
- })
274
-
275
- test("blocks active claims", async () => {
276
- if (!Bun.which("jj")) return
277
- const inspected = await runTestEffect(
278
- ClaimService.pipe(
279
- Effect.flatMap((service) =>
280
- service.inspect("example", undefined, root),
281
- ),
282
- ),
283
- )
284
- await runTestEffect(
285
- ClaimService.pipe(
286
- Effect.flatMap((service) =>
287
- service.claim(
288
- {
289
- taskId: "example",
290
- claimant: "orchestrator",
291
- agent: "agent",
292
- sessionId: "session-1",
293
- revision: inspected.revision,
294
- },
295
- root,
296
- ),
297
- ),
298
- ),
299
- )
300
- await expect(
301
- runTestEffect(
302
- VcsMigrationService.pipe(
303
- Effect.flatMap((service) =>
304
- service.migrate("jj", root, { apply: true }),
305
- ),
306
- ),
307
- ),
308
- ).rejects.toThrow("is active")
309
- })
310
-
311
- test("blocks dirty workspaces", async () => {
312
- if (!Bun.which("jj")) return
313
- await Bun.write(join(root, "tasks/example/code/agency/DIRTY.md"), "dirty\n")
314
- await expect(
315
- runTestEffect(
316
- VcsMigrationService.pipe(
317
- Effect.flatMap((service) =>
318
- service.migrate("jj", root, { apply: true }),
319
- ),
320
- ),
321
- ),
322
- ).rejects.toThrow("must be clean")
323
- })
324
-
325
- test("blocks jj-only heads when migrating to Git", async () => {
326
- if (!Bun.which("jj")) return
327
- await runTestEffect(
328
- VcsMigrationService.pipe(
329
- Effect.flatMap((service) =>
330
- service.migrate("jj", root, { apply: true }),
331
- ),
332
- ),
333
- )
334
- const repository = join(root, "repos/agency")
335
- await run(["jj", "-R", repository, "new", "main", "-m", "hidden head"])
336
- await run(["jj", "-R", repository, "new", "main"])
337
-
338
- await expect(
339
- runTestEffect(
340
- VcsMigrationService.pipe(
341
- Effect.flatMap((service) =>
342
- service.migrate("git", root, { apply: true }),
343
- ),
344
- ),
345
- ),
346
- ).rejects.toThrow("jj-only heads")
347
- })
348
- })