@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
@@ -3,10 +3,10 @@ import { Effect } from "effect"
3
3
  import { mkdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
- import { TaskService } from "./TaskService"
7
6
  import { PushService } from "./PushService"
8
- import { parseGitCommits } from "./push-validation"
7
+ import { TaskService } from "./TaskService"
9
8
  import { WorktreeService } from "./WorktreeService"
9
+ import { parseGitCommits } from "./push-validation"
10
10
 
11
11
  interface CommandResult {
12
12
  readonly stdout: string
@@ -40,7 +40,7 @@ const requireCommand = async (args: readonly string[], cwd?: string) => {
40
40
  }
41
41
 
42
42
  describe("PushService", () => {
43
- test("parses and validates batched Git commit metadata", () => {
43
+ test("parses batched Git commit metadata", () => {
44
44
  const output = [
45
45
  "abc123\0Agency Test\0agency@example.com\0First change\0\x1e",
46
46
  "def456\0Agency Test\0agency@example.com\0Second change\0\x1e",
@@ -64,17 +64,14 @@ describe("PushService", () => {
64
64
  await Promise.all(roots.splice(0).map(cleanupTempDir))
65
65
  })
66
66
 
67
- const setup = async (vcs: "git" | "jj") => {
67
+ const setup = async () => {
68
68
  const root = await createTempDir()
69
69
  roots.push(root)
70
70
  const remote = join(root, "remote.git")
71
71
  const seed = join(root, "seed")
72
72
  const repository = join(root, "repos", "agency")
73
73
  await mkdir(join(root, "repos"), { recursive: true })
74
- await Bun.write(
75
- join(root, "agency.json"),
76
- `${JSON.stringify({ version: 2, vcs }, null, 2)}\n`,
77
- )
74
+ await Bun.write(join(root, "agency.json"), '{"version":2,"vcs":"git"}\n')
78
75
  await requireCommand([
79
76
  "git",
80
77
  "init",
@@ -93,27 +90,7 @@ describe("PushService", () => {
93
90
  await requireCommand(["git", "commit", "-m", "Initial commit"], seed)
94
91
  await requireCommand(["git", "remote", "add", "origin", remote], seed)
95
92
  await requireCommand(["git", "push", "-u", "origin", "main"], seed)
96
- if (vcs === "jj") {
97
- await requireCommand([
98
- "jj",
99
- "git",
100
- "clone",
101
- "--no-colocate",
102
- remote,
103
- repository,
104
- ])
105
- expect(await Bun.file(join(repository, ".git")).exists()).toBe(false)
106
- await requireCommand(
107
- ["jj", "config", "set", "--repo", "user.name", "Agency Test"],
108
- repository,
109
- )
110
- await requireCommand(
111
- ["jj", "config", "set", "--repo", "user.email", "agency@example.com"],
112
- repository,
113
- )
114
- } else {
115
- await requireCommand(["git", "clone", "--bare", remote, repository])
116
- }
93
+ await requireCommand(["git", "clone", "--bare", remote, repository])
117
94
 
118
95
  await runTestEffect(
119
96
  TaskService.pipe(
@@ -158,27 +135,31 @@ describe("PushService", () => {
158
135
  PushService.pipe(Effect.flatMap((service) => service.publish(taskPath))),
159
136
  )
160
137
 
161
- const remoteBranch = async (remote: string, branch = "task/example") =>
138
+ const remoteBranch = async (remote: string) =>
162
139
  (
163
140
  await requireCommand([
164
141
  "git",
165
142
  "--git-dir",
166
143
  remote,
167
144
  "rev-parse",
168
- `refs/heads/${branch}`,
145
+ "refs/heads/task/example",
169
146
  ])
170
147
  ).stdout
171
148
 
172
- test("publishes a clean Git HEAD and establishes upstream tracking", async () => {
173
- const fixture = await setup("git")
149
+ const configureAuthor = async (checkout: string) => {
174
150
  await requireCommand(
175
151
  ["git", "config", "user.name", "Agency Test"],
176
- fixture.checkout,
152
+ checkout,
177
153
  )
178
154
  await requireCommand(
179
155
  ["git", "config", "user.email", "agency@example.com"],
180
- fixture.checkout,
156
+ checkout,
181
157
  )
158
+ }
159
+
160
+ test("publishes a clean Git HEAD and establishes upstream tracking", async () => {
161
+ const fixture = await setup()
162
+ await configureAuthor(fixture.checkout)
182
163
  await Bun.write(join(fixture.checkout, "feature.txt"), "published\n")
183
164
  await requireCommand(["git", "add", "feature.txt"], fixture.checkout)
184
165
  await requireCommand(
@@ -205,11 +186,11 @@ describe("PushService", () => {
205
186
  })
206
187
 
207
188
  test("rejects dirty, mismatched, and undescribed Git publication", async () => {
208
- const dirty = await setup("git")
189
+ const dirty = await setup()
209
190
  await Bun.write(join(dirty.checkout, "dirty.txt"), "dirty\n")
210
191
  await expect(publish(dirty.taskPath)).rejects.toThrow("dirty Git worktree")
211
192
 
212
- const mismatched = await setup("git")
193
+ const mismatched = await setup()
213
194
  await requireCommand(
214
195
  ["git", "branch", "-m", "wrong-branch"],
215
196
  mismatched.checkout,
@@ -218,15 +199,8 @@ describe("PushService", () => {
218
199
  "does not match checked-out Git branch",
219
200
  )
220
201
 
221
- const undescribed = await setup("git")
222
- await requireCommand(
223
- ["git", "config", "user.name", "Agency Test"],
224
- undescribed.checkout,
225
- )
226
- await requireCommand(
227
- ["git", "config", "user.email", "agency@example.com"],
228
- undescribed.checkout,
229
- )
202
+ const undescribed = await setup()
203
+ await configureAuthor(undescribed.checkout)
230
204
  await requireCommand(
231
205
  ["git", "commit", "--allow-empty", "--allow-empty-message", "-m", ""],
232
206
  undescribed.checkout,
@@ -237,13 +211,8 @@ describe("PushService", () => {
237
211
  })
238
212
 
239
213
  test("rejects Git remote divergence after refreshing remote state", async () => {
240
- const fixture = await setup("git")
241
- for (const [key, value] of [
242
- ["user.name", "Agency Test"],
243
- ["user.email", "agency@example.com"],
244
- ] as const) {
245
- await requireCommand(["git", "config", key, value], fixture.checkout)
246
- }
214
+ const fixture = await setup()
215
+ await configureAuthor(fixture.checkout)
247
216
  await Bun.write(join(fixture.checkout, "feature.txt"), "first\n")
248
217
  await requireCommand(["git", "add", "feature.txt"], fixture.checkout)
249
218
  await requireCommand(
@@ -277,7 +246,7 @@ describe("PushService", () => {
277
246
  })
278
247
 
279
248
  test("requires working status and the declared Git base in history", async () => {
280
- const open = await setup("git")
249
+ const open = await setup()
281
250
  await runTestEffect(
282
251
  TaskService.pipe(
283
252
  Effect.flatMap((service) =>
@@ -289,15 +258,8 @@ describe("PushService", () => {
289
258
  "status 'open'; status must be working",
290
259
  )
291
260
 
292
- const unrelated = await setup("git")
293
- await requireCommand(
294
- ["git", "config", "user.name", "Agency Test"],
295
- unrelated.checkout,
296
- )
297
- await requireCommand(
298
- ["git", "config", "user.email", "agency@example.com"],
299
- unrelated.checkout,
300
- )
261
+ const unrelated = await setup()
262
+ await configureAuthor(unrelated.checkout)
301
263
  await requireCommand(
302
264
  ["git", "checkout", "--orphan", "unrelated"],
303
265
  unrelated.checkout,
@@ -318,16 +280,9 @@ describe("PushService", () => {
318
280
  )
319
281
  })
320
282
 
321
- test("rejects invalid Git and jj authors", async () => {
322
- const git = await setup("git")
323
- await requireCommand(
324
- ["git", "config", "user.name", "Agency Test"],
325
- git.checkout,
326
- )
327
- await requireCommand(
328
- ["git", "config", "user.email", "agency@example.com"],
329
- git.checkout,
330
- )
283
+ test("rejects invalid Git authors", async () => {
284
+ const fixture = await setup()
285
+ await configureAuthor(fixture.checkout)
331
286
  await requireCommand(
332
287
  [
333
288
  "git",
@@ -338,231 +293,10 @@ describe("PushService", () => {
338
293
  "-m",
339
294
  "Invalid author",
340
295
  ],
341
- git.checkout,
342
- )
343
- await expect(publish(git.taskPath)).rejects.toThrow("has an invalid author")
344
-
345
- if (!Bun.which("jj")) return
346
- const jj = await setup("jj")
347
- await requireCommand(
348
- ["jj", "describe", "-m", "Invalid author"],
349
- jj.checkout,
350
- )
351
- await requireCommand(
352
- ["jj", "metaedit", "--author", "Bad <bad>"],
353
- jj.checkout,
354
- )
355
- const changeId = (
356
- await requireCommand(
357
- ["jj", "log", "--no-graph", "-r", "@", "-T", "change_id"],
358
- jj.checkout,
359
- )
360
- ).stdout
361
- await expect(publish(jj.taskPath)).rejects.toThrow(
362
- `Change ${changeId} has an invalid author. Run: jj metaedit -r ${changeId} --author 'Name <email>'`,
363
- )
364
- })
365
-
366
- test("publishes a described jj working-copy change under the declared bookmark", async () => {
367
- if (!Bun.which("jj")) return
368
- const fixture = await setup("jj")
369
- await Bun.write(join(fixture.checkout, "feature.txt"), "published\n")
370
- await requireCommand(
371
- ["jj", "describe", "-m", "Add published feature"],
372
- fixture.checkout,
373
- )
374
-
375
- const result = await publish(fixture.taskPath)
376
- expect(result).toMatchObject({
377
- vcs: "jj",
378
- branch: "task/example",
379
- base: "main",
380
- remote: "origin",
381
- })
382
- expect(await remoteBranch(fixture.remote)).toBe(result.tip)
383
- expect(
384
- (
385
- await requireCommand(
386
- [
387
- "jj",
388
- "bookmark",
389
- "list",
390
- "--all-remotes",
391
- "-T",
392
- 'if(name == "task/example" && remote == "origin", tracked, "")',
393
- ],
394
- fixture.checkout,
395
- )
396
- ).stdout,
397
- ).toBe("true")
398
-
399
- await requireCommand(["jj", "new", "@"], fixture.checkout)
400
- await Bun.write(join(fixture.checkout, "follow-up.txt"), "follow up\n")
401
- await requireCommand(
402
- ["jj", "describe", "-m", "Add follow-up"],
403
- fixture.checkout,
404
- )
405
- const advanced = await publish(fixture.taskPath)
406
- expect(advanced.tip).not.toBe(result.tip)
407
- expect(await remoteBranch(fixture.remote)).toBe(advanced.tip)
408
- }, 15_000)
409
-
410
- test("diagnoses a jj stack whose remote base advanced", async () => {
411
- if (!Bun.which("jj")) return
412
- const fixture = await setup("jj")
413
- await Bun.write(join(fixture.checkout, "feature.txt"), "local\n")
414
- await requireCommand(
415
- ["jj", "describe", "-m", "Local feature"],
416
- fixture.checkout,
417
- )
418
-
419
- const other = join(fixture.root, "other-main")
420
- await requireCommand(["git", "clone", fixture.remote, other])
421
- await requireCommand(["git", "config", "user.name", "Other"], other)
422
- await requireCommand(
423
- ["git", "config", "user.email", "other@example.com"],
424
- other,
425
- )
426
- await Bun.write(join(other, "remote.txt"), "remote\n")
427
- await requireCommand(["git", "add", "remote.txt"], other)
428
- await requireCommand(["git", "commit", "-m", "Advance main"], other)
429
- await requireCommand(["git", "push", "origin", "main"], other)
430
-
431
- await expect(publish(fixture.taskPath)).rejects.toThrow(
432
- "jj rebase -s 'roots(main@origin..@)' -d main@origin",
433
- )
434
- })
435
-
436
- test("selects the described parent of a canonical jj post-commit working copy", async () => {
437
- if (!Bun.which("jj")) return
438
- const fixture = await setup("jj")
439
- await Bun.write(join(fixture.checkout, "feature.txt"), "published\n")
440
- await requireCommand(
441
- ["jj", "commit", "-m", "Add published feature"],
442
296
  fixture.checkout,
443
297
  )
444
- const parent = (
445
- await requireCommand(
446
- ["jj", "log", "--no-graph", "-r", "@-", "-T", "commit_id"],
447
- fixture.checkout,
448
- )
449
- ).stdout
450
-
451
- const result = await publish(fixture.taskPath)
452
- expect(result.tip).toBe(parent)
453
- expect(await remoteBranch(fixture.remote)).toBe(parent)
454
- })
455
-
456
- test("preserves a described empty jj change and diagnoses missing semantics", async () => {
457
- if (!Bun.which("jj")) return
458
- const described = await setup("jj")
459
- await requireCommand(
460
- ["jj", "describe", "-m", "Record intentional empty change"],
461
- described.checkout,
462
- )
463
- const describedTip = (
464
- await requireCommand(
465
- ["jj", "log", "--no-graph", "-r", "@", "-T", "commit_id"],
466
- described.checkout,
467
- )
468
- ).stdout
469
- expect((await publish(described.taskPath)).tip).toBe(describedTip)
470
-
471
- const undescribed = await setup("jj")
472
- await Bun.write(join(undescribed.checkout, "feature.txt"), "missing\n")
473
- const changeId = (
474
- await requireCommand(
475
- ["jj", "log", "--no-graph", "-r", "@", "-T", "change_id"],
476
- undescribed.checkout,
477
- )
478
- ).stdout
479
- await expect(publish(undescribed.taskPath)).rejects.toThrow(
480
- `Change ${changeId} has no description. Run: jj describe -r ${changeId}`,
481
- )
482
- }, 15_000)
483
-
484
- test("rejects an empty jj task and remote bookmark divergence", async () => {
485
- if (!Bun.which("jj")) return
486
- const empty = await setup("jj")
487
- await expect(publish(empty.taskPath)).rejects.toThrow(
488
- "No changes to publish after base 'main'",
489
- )
490
-
491
- const fixture = await setup("jj")
492
- await Bun.write(join(fixture.checkout, "feature.txt"), "first\n")
493
- await requireCommand(
494
- ["jj", "describe", "-m", "First change"],
495
- fixture.checkout,
496
- )
497
- await publish(fixture.taskPath)
498
-
499
- const other = join(fixture.root, "other")
500
- await requireCommand(["git", "clone", fixture.remote, other])
501
- await requireCommand(["git", "checkout", "task/example"], other)
502
- await requireCommand(["git", "config", "user.name", "Other"], other)
503
- await requireCommand(
504
- ["git", "config", "user.email", "other@example.com"],
505
- other,
506
- )
507
- await Bun.write(join(other, "remote.txt"), "remote\n")
508
- await requireCommand(["git", "add", "remote.txt"], other)
509
- await requireCommand(["git", "commit", "-m", "Remote change"], other)
510
- await requireCommand(["git", "push", "origin", "task/example"], other)
511
-
512
- await requireCommand(["jj", "new", "@"], fixture.checkout)
513
- await Bun.write(join(fixture.checkout, "local.txt"), "local\n")
514
- await requireCommand(
515
- ["jj", "describe", "-m", "Local change"],
516
- fixture.checkout,
517
- )
518
- await expect(publish(fixture.taskPath)).rejects.toThrow(
519
- "refusing to move it",
520
- )
521
- }, 15_000)
522
-
523
- test("rejects conflicted jj changes with an actionable change ID", async () => {
524
- if (!Bun.which("jj")) return
525
- const fixture = await setup("jj")
526
- await Bun.write(join(fixture.checkout, "README.md"), "left\n")
527
- await requireCommand(
528
- ["jj", "describe", "-m", "Left change"],
529
- fixture.checkout,
530
- )
531
- await requireCommand(
532
- ["jj", "bookmark", "create", "left", "-r", "@"],
533
- fixture.checkout,
534
- )
535
- await requireCommand(["jj", "new", "main@origin"], fixture.checkout)
536
- await Bun.write(join(fixture.checkout, "README.md"), "right\n")
537
- await requireCommand(
538
- ["jj", "describe", "-m", "Right change"],
539
- fixture.checkout,
540
- )
541
- await requireCommand(
542
- ["jj", "bookmark", "create", "right", "-r", "@"],
543
- fixture.checkout,
544
- )
545
- await requireCommand(["jj", "new", "left", "right"], fixture.checkout)
546
- await requireCommand(
547
- ["jj", "describe", "-m", "Conflicted merge"],
548
- fixture.checkout,
549
- )
550
- const changeId = (
551
- await requireCommand(
552
- ["jj", "log", "--no-graph", "-r", "@", "-T", "change_id"],
553
- fixture.checkout,
554
- )
555
- ).stdout
556
- expect(
557
- (
558
- await requireCommand(
559
- ["jj", "log", "--no-graph", "-r", "@", "-T", "conflict"],
560
- fixture.checkout,
561
- )
562
- ).stdout,
563
- ).toBe("true")
564
298
  await expect(publish(fixture.taskPath)).rejects.toThrow(
565
- `Change ${changeId} contains conflicts. Run: jj resolve -r ${changeId}`,
299
+ "has an invalid author",
566
300
  )
567
301
  })
568
302
  })