@markjaquith/agency 2.74.0 → 3.1.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 +29 -122
  2. package/cli-main.ts +10 -28
  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 +5 -19
  7. package/src/cli-parser.ts +12 -30
  8. package/src/cli.test.ts +6 -6
  9. package/src/commands/archive.test.ts +73 -1
  10. package/src/commands/archive.ts +18 -7
  11. package/src/commands/context.test.ts +0 -67
  12. package/src/commands/init.test.ts +1 -2
  13. package/src/commands/pr.test.ts +2 -87
  14. package/src/commands/pr.ts +2 -79
  15. package/src/commands/push.test.ts +1 -2
  16. package/src/graph-schema.ts +1 -1
  17. package/src/services/ArchiveBulkService.test.ts +1 -142
  18. package/src/services/ArchiveService.test.ts +1 -172
  19. package/src/services/ArchiveService.ts +59 -27
  20. package/src/services/ContextService.ts +3 -47
  21. package/src/services/DoctorService.ts +8 -24
  22. package/src/services/GraphService.test.ts +1 -0
  23. package/src/services/GraphService.ts +2 -5
  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 +4 -5
  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
@@ -1,7 +1,5 @@
1
1
  import { Data, Effect } from "effect"
2
2
  import { FileSystemService } from "./FileSystemService"
3
- import { WorkbaseService } from "./WorkbaseService"
4
- import type { VersionControlKind } from "../workbase/version-control"
5
3
 
6
4
  export interface RegisteredWorkspace {
7
5
  readonly name: string | null
@@ -12,35 +10,20 @@ export interface RegisteredWorkspace {
12
10
  readonly dirty?: boolean
13
11
  }
14
12
 
15
- interface PullRequestDefaults {
16
- readonly title: string
17
- readonly body: string
18
- }
19
-
20
13
  interface RepositoryInspection {
21
14
  readonly kind: "bare" | "repository"
22
15
  readonly remote: string | null
23
16
  }
24
17
 
25
18
  export interface VersionControlBackend {
26
- readonly kind: VersionControlKind
19
+ readonly kind: "git"
27
20
  readonly cloneRepository: (
28
21
  source: string,
29
22
  destination: string,
30
23
  ) => Effect.Effect<void, unknown, any>
31
- readonly initializeRepository: (
32
- path: string,
33
- ) => Effect.Effect<void, unknown, any>
34
24
  readonly inspectRepository: (
35
25
  path: string,
36
26
  ) => Effect.Effect<RepositoryInspection | null, unknown, any>
37
- readonly gitEnvironment: (
38
- path: string,
39
- ) => Effect.Effect<Record<string, string>, unknown, any>
40
- readonly pullRequestDefaults: (
41
- workspacePath: string,
42
- base: string,
43
- ) => Effect.Effect<PullRequestDefaults | null, unknown, any>
44
27
  readonly listWorkspaces: (
45
28
  repositoryPath: string,
46
29
  ) => Effect.Effect<readonly RegisteredWorkspace[], unknown, any>
@@ -54,26 +37,11 @@ export interface VersionControlBackend {
54
37
  readonly workspaceDirty: (
55
38
  workspacePath: string,
56
39
  ) => Effect.Effect<boolean | null, unknown, any>
57
- readonly createWorkspace: (options: {
58
- readonly repositoryPath: string
59
- readonly workspacePath: string
60
- readonly workspaceName: string
61
- readonly revision: string
62
- readonly branch?: string
63
- }) => Effect.Effect<void, unknown, any>
64
- readonly removeWorkspace: (options: {
65
- readonly repositoryPath: string
66
- readonly workspacePath: string
67
- readonly workspaceName: string | null
68
- }) => Effect.Effect<void, unknown, any>
69
40
  readonly fetch: (
70
41
  repositoryPath: string,
71
42
  remote?: string,
72
43
  branch?: string,
73
44
  ) => Effect.Effect<void, unknown, any>
74
- readonly importGitRefs: (
75
- repositoryPath: string,
76
- ) => Effect.Effect<void, unknown, any>
77
45
  readonly push: (
78
46
  workspacePath: string,
79
47
  remote: string,
@@ -210,7 +178,6 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
210
178
  ),
211
179
  )
212
180
  }),
213
- initializeRepository: () => Effect.void,
214
181
  inspectRepository: (path) =>
215
182
  Effect.gen(function* () {
216
183
  const fs = yield* FileSystemService
@@ -243,8 +210,6 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
243
210
  remote === undefined ? null : expandGitUrl(remote, entries),
244
211
  } as const
245
212
  }),
246
- gitEnvironment: () => Effect.succeed({}),
247
- pullRequestDefaults: () => Effect.succeed(null),
248
213
  listWorkspaces: (repositoryPath) =>
249
214
  Effect.gen(function* () {
250
215
  const fs = yield* FileSystemService
@@ -299,54 +264,6 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
299
264
  )
300
265
  return result.exitCode === 0 ? result.stdout.length > 0 : null
301
266
  }),
302
- createWorkspace: (options) =>
303
- Effect.gen(function* () {
304
- const fs = yield* FileSystemService
305
- const command = options.branch
306
- ? [
307
- "git",
308
- "-C",
309
- options.repositoryPath,
310
- "worktree",
311
- "add",
312
- "-b",
313
- options.branch,
314
- options.workspacePath,
315
- options.revision,
316
- ]
317
- : [
318
- "git",
319
- "-C",
320
- options.repositoryPath,
321
- "worktree",
322
- "add",
323
- "--detach",
324
- options.workspacePath,
325
- options.revision,
326
- ]
327
- yield* requireSuccess(
328
- "Failed to create Git worktree",
329
- fs.runCommand(command, { captureOutput: true }),
330
- )
331
- }),
332
- removeWorkspace: (options) =>
333
- Effect.gen(function* () {
334
- const fs = yield* FileSystemService
335
- yield* requireSuccess(
336
- "Failed to remove Git worktree",
337
- fs.runCommand(
338
- [
339
- "git",
340
- "-C",
341
- options.repositoryPath,
342
- "worktree",
343
- "remove",
344
- options.workspacePath,
345
- ],
346
- { captureOutput: true },
347
- ),
348
- )
349
- }),
350
267
  fetch: (repositoryPath, remote = "origin", branch) =>
351
268
  Effect.gen(function* () {
352
269
  const fs = yield* FileSystemService
@@ -365,7 +282,6 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
365
282
  ),
366
283
  )
367
284
  }),
368
- importGitRefs: () => Effect.void,
369
285
  push: (workspacePath, remote, branch) =>
370
286
  Effect.gen(function* () {
371
287
  const fs = yield* FileSystemService
@@ -422,352 +338,13 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
422
338
  },
423
339
  ) {}
424
340
 
425
- const jjCommand = (repositoryPath: string, args: readonly string[]) => [
426
- "jj",
427
- "-R",
428
- repositoryPath,
429
- "--no-pager",
430
- ...args,
431
- ]
432
-
433
- export class JjVersionControlService extends Effect.Service<JjVersionControlService>()(
434
- "JjVersionControlService",
435
- {
436
- sync: () =>
437
- ({
438
- kind: "jj",
439
- cloneRepository: (source, destination) =>
440
- Effect.gen(function* () {
441
- const fs = yield* FileSystemService
442
- yield* requireSuccess(
443
- "Failed to clone jj repository",
444
- fs.runCommand(
445
- [
446
- "jj",
447
- "git",
448
- "clone",
449
- "--no-colocate",
450
- "--",
451
- source,
452
- destination,
453
- ],
454
- { captureOutput: true },
455
- ),
456
- )
457
- }),
458
- initializeRepository: (path) =>
459
- Effect.gen(function* () {
460
- const fs = yield* FileSystemService
461
- if (yield* fs.exists(`${path}/.jj`)) return
462
- yield* requireSuccess(
463
- "Failed to initialize jj repository",
464
- fs.runCommand(["jj", "git", "init", "--colocate", path], {
465
- captureOutput: true,
466
- }),
467
- )
468
- }),
469
- inspectRepository: (path) =>
470
- Effect.gen(function* () {
471
- const fs = yield* FileSystemService
472
- const root = yield* fs.runCommand(jjCommand(path, ["root"]), {
473
- captureOutput: true,
474
- })
475
- if (root.exitCode !== 0) return null
476
- const remote = yield* fs.runCommand(
477
- jjCommand(path, ["git", "remote", "list"]),
478
- { captureOutput: true },
479
- )
480
- return {
481
- kind: "repository",
482
- remote:
483
- remote.exitCode === 0
484
- ? (remote.stdout
485
- .split("\n")
486
- .find((line) => line.startsWith("origin "))
487
- ?.slice("origin ".length)
488
- .trim() ?? null)
489
- : null,
490
- } as const
491
- }),
492
- gitEnvironment: (path) =>
493
- Effect.gen(function* () {
494
- const fs = yield* FileSystemService
495
- const result = yield* requireSuccess(
496
- "Failed to locate jj backing Git repository",
497
- fs.runCommand(jjCommand(path, ["git", "root"]), {
498
- captureOutput: true,
499
- }),
500
- )
501
- return { GIT_DIR: result.stdout.trim() }
502
- }),
503
- pullRequestDefaults: (workspacePath, base) =>
504
- Effect.gen(function* () {
505
- const fs = yield* FileSystemService
506
- const result = yield* fs.runCommand(
507
- jjCommand(workspacePath, [
508
- "log",
509
- "--ignore-working-copy",
510
- "--no-graph",
511
- "-r",
512
- `${base}..@-`,
513
- "-T",
514
- 'description.first_line() ++ "\\n"',
515
- ]),
516
- { captureOutput: true },
517
- )
518
- if (result.exitCode !== 0) return null
519
- const commits = result.stdout
520
- .split("\n")
521
- .map((line) => line.trim())
522
- .filter(Boolean)
523
- if (commits.length === 0) return null
524
- return {
525
- title: commits.at(-1)!,
526
- body:
527
- commits.length === 1
528
- ? ""
529
- : `Commits:\n${commits
530
- .toReversed()
531
- .map((commit) => `- ${commit}`)
532
- .join("\n")}`,
533
- } satisfies PullRequestDefaults
534
- }),
535
- listWorkspaces: (repositoryPath) =>
536
- Effect.gen(function* () {
537
- const fs = yield* FileSystemService
538
- const result = yield* requireSuccess(
539
- "Failed to inspect jj workspaces",
540
- fs.runCommand(
541
- jjCommand(repositoryPath, [
542
- "workspace",
543
- "list",
544
- "-T",
545
- 'name ++ "\\t" ++ root ++ "\\t" ++ target.commit_id() ++ "\\t" ++ target.parents().map(|commit| commit.commit_id()).join(",") ++ "\\t" ++ target.empty() ++ "\\n"',
546
- ]),
547
- { captureOutput: true },
548
- ),
549
- )
550
- return result.stdout
551
- .trim()
552
- .split("\n")
553
- .filter(Boolean)
554
- .map((line) => {
555
- const [name, path, commit, parents, empty] = line.split("\t")
556
- const parentCommits = parents?.split(",").filter(Boolean) ?? []
557
- return {
558
- name: name || null,
559
- path: path!,
560
- commit: commit || null,
561
- branch: null,
562
- head:
563
- parentCommits.length === 1 ? parentCommits[0]! : undefined,
564
- dirty: empty === "false",
565
- }
566
- })
567
- }),
568
- resolveRevision: (repositoryPath, revision) =>
569
- Effect.gen(function* () {
570
- const fs = yield* FileSystemService
571
- const resolve = () =>
572
- fs.runCommand(
573
- jjCommand(repositoryPath, [
574
- "log",
575
- "--ignore-working-copy",
576
- "--no-graph",
577
- "-r",
578
- revision,
579
- "-T",
580
- 'commit_id ++ "\\n"',
581
- ]),
582
- { captureOutput: true },
583
- )
584
- let result = yield* resolve()
585
- if (result.exitCode !== 0 || !result.stdout.trim()) {
586
- const imported = yield* fs.runCommand(
587
- jjCommand(repositoryPath, ["git", "import"]),
588
- { captureOutput: true },
589
- )
590
- if (imported.exitCode === 0) result = yield* resolve()
591
- }
592
- return result.exitCode === 0 ? result.stdout.trim() || null : null
593
- }),
594
- workspaceHead: (workspacePath) =>
595
- Effect.gen(function* () {
596
- const fs = yield* FileSystemService
597
- const result = yield* fs.runCommand(
598
- jjCommand(workspacePath, [
599
- "log",
600
- "--ignore-working-copy",
601
- "--no-graph",
602
- "-r",
603
- "@-",
604
- "-T",
605
- 'commit_id ++ "\\n"',
606
- ]),
607
- { captureOutput: true },
608
- )
609
- return result.exitCode === 0 ? result.stdout.trim() || null : null
610
- }),
611
- workspaceDirty: (workspacePath) =>
612
- Effect.gen(function* () {
613
- const fs = yield* FileSystemService
614
- const result = yield* fs.runCommand(
615
- jjCommand(workspacePath, ["diff", "--summary", "-r", "@"]),
616
- { captureOutput: true },
617
- )
618
- return result.exitCode === 0 ? result.stdout.length > 0 : null
619
- }),
620
- createWorkspace: (options) =>
621
- Effect.gen(function* () {
622
- const fs = yield* FileSystemService
623
- yield* requireSuccess(
624
- "Failed to create jj workspace",
625
- fs.runCommand(
626
- jjCommand(options.repositoryPath, [
627
- "workspace",
628
- "add",
629
- "--name",
630
- options.workspaceName,
631
- "-r",
632
- options.revision,
633
- options.workspacePath,
634
- ]),
635
- { captureOutput: true },
636
- ),
637
- )
638
- }),
639
- removeWorkspace: (options) =>
640
- Effect.gen(function* () {
641
- const fs = yield* FileSystemService
642
- if (!options.workspaceName) {
643
- return yield* new VersionControlError({
644
- message: `Cannot remove unregistered jj workspace ${options.workspacePath}`,
645
- })
646
- }
647
- yield* requireSuccess(
648
- "Failed to forget jj workspace",
649
- fs.runCommand(
650
- jjCommand(options.repositoryPath, [
651
- "workspace",
652
- "forget",
653
- options.workspaceName,
654
- ]),
655
- { captureOutput: true },
656
- ),
657
- )
658
- yield* fs.deleteDirectory(options.workspacePath)
659
- }),
660
- fetch: (repositoryPath, remote = "origin", branch) =>
661
- Effect.gen(function* () {
662
- const fs = yield* FileSystemService
663
- yield* requireSuccess(
664
- "Failed to fetch jj repository",
665
- fs.runCommand(
666
- jjCommand(repositoryPath, [
667
- "git",
668
- "fetch",
669
- "--remote",
670
- remote,
671
- ...(branch ? ["--branch", branch] : []),
672
- ]),
673
- { captureOutput: true },
674
- ),
675
- )
676
- }),
677
- importGitRefs: (repositoryPath) =>
678
- Effect.gen(function* () {
679
- const fs = yield* FileSystemService
680
- yield* requireSuccess(
681
- "Failed to import Git refs into jj",
682
- fs.runCommand(jjCommand(repositoryPath, ["git", "import"]), {
683
- captureOutput: true,
684
- }),
685
- )
686
- }),
687
- push: (workspacePath, remote, branch) =>
688
- Effect.gen(function* () {
689
- const fs = yield* FileSystemService
690
- yield* requireSuccess(
691
- "Failed to set jj delivery bookmark",
692
- fs.runCommand(
693
- jjCommand(workspacePath, [
694
- "bookmark",
695
- "set",
696
- "--allow-backwards",
697
- branch,
698
- "-r",
699
- "@-",
700
- ]),
701
- { captureOutput: true },
702
- ),
703
- )
704
- yield* requireSuccess(
705
- "Failed to push branch",
706
- fs.runCommand(
707
- jjCommand(workspacePath, [
708
- "git",
709
- "push",
710
- "--remote",
711
- remote,
712
- "--bookmark",
713
- branch,
714
- ]),
715
- { captureOutput: true },
716
- ),
717
- )
718
- }),
719
- remoteUrl: (repositoryPath, remote) =>
720
- Effect.gen(function* () {
721
- const fs = yield* FileSystemService
722
- const result = yield* fs.runCommand(
723
- jjCommand(repositoryPath, ["git", "remote", "list"]),
724
- { captureOutput: true },
725
- )
726
- if (result.exitCode !== 0) return null
727
- return (
728
- result.stdout
729
- .split("\n")
730
- .find((line) => line.startsWith(`${remote} `))
731
- ?.slice(remote.length + 1)
732
- .trim() ?? null
733
- )
734
- }),
735
- setRemoteUrl: (repositoryPath, remote, url) =>
736
- Effect.gen(function* () {
737
- const fs = yield* FileSystemService
738
- const previous = yield* fs.runCommand(
739
- jjCommand(repositoryPath, ["git", "remote", "list"]),
740
- { captureOutput: true },
741
- )
742
- const exists = previous.stdout
743
- .split("\n")
744
- .some((line) => line.startsWith(`${remote} `))
745
- const args =
746
- url === null
747
- ? ["git", "remote", "remove", remote]
748
- : ["git", "remote", exists ? "set-url" : "add", remote, url]
749
- yield* requireSuccess(
750
- `Failed to update jj Git remote '${remote}'`,
751
- fs.runCommand(jjCommand(repositoryPath, args), {
752
- captureOutput: true,
753
- }),
754
- )
755
- }),
756
- }) satisfies VersionControlBackend,
757
- },
758
- ) {}
759
-
760
341
  export class VersionControlService extends Effect.Service<VersionControlService>()(
761
342
  "VersionControlService",
762
343
  {
763
344
  sync: () => ({
764
- forWorkbase: (root: string) =>
345
+ forWorkbase: (_root: string) =>
765
346
  Effect.gen(function* () {
766
- const workbase = yield* WorkbaseService
767
- const git = yield* GitVersionControlService
768
- const jj = yield* JjVersionControlService
769
- const { config } = yield* workbase.loadConfig(root)
770
- return config.vcs === "jj" ? jj : git
347
+ return yield* GitVersionControlService
771
348
  }),
772
349
  }),
773
350
  },
@@ -462,26 +462,6 @@ status: done
462
462
  ).rejects.toThrow("{worktree}")
463
463
  })
464
464
 
465
- test("rejects an invalid workspace command template", async () => {
466
- await write(
467
- root,
468
- "agency.json",
469
- JSON.stringify({
470
- version: 2,
471
- vcs: "jj",
472
- workspaceCreateCommand: ["tool", "{repo}", "{workspace}", "{name}"],
473
- }),
474
- )
475
-
476
- await expect(
477
- runTestEffect(
478
- WorkbaseService.pipe(
479
- Effect.flatMap((service) => service.discover(root)),
480
- ),
481
- ),
482
- ).rejects.toThrow("{revision}")
483
- })
484
-
485
465
  test("rejects an unknown post-checkout command placeholder", async () => {
486
466
  await write(
487
467
  root,
@@ -21,12 +21,10 @@ import {
21
21
  type WorkbaseRegistration,
22
22
  } from "../workbase/schemas"
23
23
  import { validateWorktreeCreateCommand } from "../workbase/worktree-command"
24
- import { validateWorkspaceCreateCommand } from "../workbase/workspace-command"
25
24
  import { validatePostCheckoutCommand } from "../workbase/checkout-command"
26
25
  import { validateAgents } from "../workbase/agent-command"
27
26
  import { findDependencyCycles } from "../workbase/dependency-graph"
28
27
  import { validateDelivery } from "../workbase/delivery-command"
29
- import { preferredVersionControl } from "../workbase/version-control"
30
28
  import { documentRevision } from "../workbase/document-revision"
31
29
 
32
30
  class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
@@ -247,7 +245,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
247
245
  [
248
246
  fs.writeJSON(configPath, {
249
247
  version: 2,
250
- vcs: preferredVersionControl(),
248
+ vcs: "git",
251
249
  }),
252
250
  ...["repos", "epics", "tasks"].map((directory) =>
253
251
  fs.createDirectory(join(root, directory)),
@@ -332,21 +330,6 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
332
330
  })
333
331
  }
334
332
  }
335
- if (decoded.value.workspaceCreateCommand) {
336
- try {
337
- validateWorkspaceCreateCommand(
338
- decoded.value.workspaceCreateCommand,
339
- )
340
- } catch (cause) {
341
- return yield* new WorkbaseConfigError({
342
- path: configPath,
343
- message:
344
- cause instanceof Error
345
- ? cause.message
346
- : "Invalid workspaceCreateCommand",
347
- })
348
- }
349
- }
350
333
  for (const [alias, repository] of Object.entries(
351
334
  decoded.value.repositories ?? {},
352
335
  )) {