@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
package/README.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  Agency manages durable agentic work across repositories. Epics, tasks, and
4
4
  phases live as Markdown documents in a filesystem-backed workbase. Repository
5
- aliases and Git worktrees provide each execution unit with the code it may read
6
- or write.
5
+ aliases and managed workspaces provide each execution unit with the code it may
6
+ read or write.
7
7
 
8
8
  ## Requirements
9
9
 
10
10
  - [Bun](https://bun.sh) 1.0 or newer
11
11
  - Git
12
+ - [Jujutsu](https://jj-vcs.github.io/jj/) is preferred when available
12
13
  - [GitHub CLI](https://cli.github.com/) for `agency pr create`
13
14
  - OpenCode, Claude Code, or a configured runner for `agency work`
14
15
 
@@ -122,14 +123,15 @@ The plugin grants whole-workbase access dynamically, while the portable
122
123
  reference advertises that context to agents. Bash and Agency operations must
123
124
  still follow the write authority reported by `agency context`.
124
125
 
125
- Repository aliases and canonical fetch remotes are declared in tracked
126
- `agency.json`; local bare clones and symlinks remain ignored under
126
+ Repository aliases, the version-control backend, and canonical fetch remotes are
127
+ declared in tracked `agency.json`; local clones and symlinks remain ignored under
127
128
  `repos/{alias}`. A declaration contains no local path, symlink target, checkout,
128
129
  or credential:
129
130
 
130
131
  ```json
131
132
  {
132
133
  "version": 2,
134
+ "vcs": "jj",
133
135
  "repositories": {
134
136
  "frontend": {
135
137
  "remote": "git@example.com:team/frontend.git"
@@ -138,6 +140,29 @@ or credential:
138
140
  }
139
141
  ```
140
142
 
143
+ New workbases select `"jj"` when the `jj` executable is available and otherwise
144
+ select `"git"`. Existing version 2 workbases without `vcs` remain Git workbases.
145
+ The selected backend is a workbase-level invariant: jj workbases initialize
146
+ managed clones with `jj git init` and create jj workspaces, while Git workbases
147
+ continue to use Git worktrees.
148
+
149
+ Inspect the current backend and migration readiness with:
150
+
151
+ ```bash
152
+ agency vcs status
153
+ agency vcs migrate jj # dry-run
154
+ agency vcs migrate jj --apply
155
+ agency vcs migrate git --apply
156
+ ```
157
+
158
+ Migration is workbase-wide and transactional. Agency locks every execution
159
+ unit, requires clean registered workspaces, blocks active claims and working or
160
+ delegated units, converts repository metadata, recreates checkout paths with the
161
+ target backend, and updates `agency.json` last. Migration from jj to Git also
162
+ blocks jj-only heads that are not preserved by a bookmark or workspace. Failed
163
+ migrations restore the source repositories and workspaces when rollback is
164
+ possible and report explicit manual recovery paths otherwise.
165
+
141
166
  Existing version 2 workbases without `repositories` remain valid. Run
142
167
  `agency repo setup` to preview deterministic adoption of legacy local aliases;
143
168
  `agency repo setup --apply` writes declarations only when a portable origin is
@@ -146,7 +171,7 @@ creation command.
146
171
 
147
172
  ### Custom Worktree Command
148
173
 
149
- By default, Agency creates worktrees with Git. Set `worktreeCreateCommand` to an
174
+ Git workbases create worktrees with Git. Set `worktreeCreateCommand` to an
150
175
  argv template when another tool should create writable worktrees:
151
176
 
152
177
  ```json
@@ -205,9 +230,10 @@ Custom commands own writable branch creation. Agency checks for conflicting
205
230
  worktrees first, invokes the command only when the branch is not checked out,
206
231
  and verifies that `{worktree}` exists afterward.
207
232
 
208
- The configured command applies only to the writable checkout. Supplemental
209
- read-only repositories remain detached Git worktrees at their declared refs so
210
- they do not acquire writable branches.
233
+ The configured command applies only to the writable checkout of a Git workbase.
234
+ Supplemental read-only repositories remain detached Git worktrees at their
235
+ declared refs so they do not acquire writable branches. Jj workbases always use
236
+ jj workspaces and ignore this Git-specific customization.
211
237
 
212
238
  ### Agent Runners
213
239
 
@@ -589,6 +615,26 @@ agency task create <id> --multi-phase
589
615
  [--ticket-url <url>] [--description <text>] [--epic <id>] [--json]
590
616
  ```
591
617
 
618
+ Create a pinned, read-only review task from the selected alias's origin:
619
+
620
+ ```text
621
+ agency task create <id> --review <alias> --pull-request <url-or-number>
622
+ agency task create <id> --review <alias> --ref <remote-ref>
623
+ agency review refresh <id> [--if-revision <hash>] [--json]
624
+ ```
625
+
626
+ Review creation fetches the source and records its exact 40-character commit.
627
+ GitHub pull requests use the base alias's `refs/pull/<number>/head`, including
628
+ fork pull requests exposed through that ref. Review workspaces contain one
629
+ detached checkout and no writable branch. Source movement is observed separately
630
+ from the pin and applied only by `review refresh`; sync, doctor, work, cleanup,
631
+ and archive never move the pin implicitly. Dirty or structurally unexpected
632
+ review checkouts block refresh and cleanup. Review tasks support normal status
633
+ and claim lifecycle, but reject phase conversion and delivery PR operations.
634
+ Each active or archived review task owns one internal task-scoped pin ref. A
635
+ refresh advances that ref transactionally; failed creation removes it. Archiving
636
+ retains the pin so a deleted source can still be restored and inspected.
637
+
592
638
  ### Noninteractive Use
593
639
 
594
640
  Agency never prompts when `--no-input` is set or stdin/stderr are not TTYs.
@@ -771,7 +817,8 @@ their built-in presets. Launches are interactive and promptless by default; use
771
817
  `--auto` to send Agency's generated context prompt.
772
818
 
773
819
  `agency work prepare` resolves an execution unit and creates or reuses its
774
- writable and reference worktrees without launching an agent or changing status.
820
+ writable and reference worktrees, or its single pinned review checkout, without
821
+ launching an agent or changing status.
775
822
  Its JSON result includes document and checkout paths, resolved commits, actions,
776
823
  and Git operations. Use `--dry-run` to report planned fetch, branch, and worktree
777
824
  changes without applying them.
package/cli.ts CHANGED
@@ -43,6 +43,15 @@ import { SyncService } from "./src/services/SyncService"
43
43
  import { ReadinessService } from "./src/services/ReadinessService"
44
44
  import { GraphMutationService } from "./src/services/GraphMutationService"
45
45
  import { DoctorService } from "./src/services/DoctorService"
46
+ import { ReviewService } from "./src/services/ReviewService"
47
+ import {
48
+ GitVersionControlService,
49
+ JjVersionControlService,
50
+ VersionControlService,
51
+ } from "./src/services/VersionControlService"
52
+ import { review, help as reviewHelp } from "./src/commands/review"
53
+ import { vcs, help as vcsHelp } from "./src/commands/vcs"
54
+ import { VcsMigrationService } from "./src/services/VcsMigrationService"
46
55
  import {
47
56
  claimCommand,
48
57
  claimHelp,
@@ -60,6 +69,10 @@ import {
60
69
  const CliLayer = Layer.mergeAll(
61
70
  FileSystemService.Default,
62
71
  WorkbaseService.Default,
72
+ GitVersionControlService.Default,
73
+ JjVersionControlService.Default,
74
+ VersionControlService.Default,
75
+ VcsMigrationService.Default,
63
76
  RepositoryService.Default,
64
77
  EpicService.Default,
65
78
  TaskService.Default,
@@ -75,6 +88,7 @@ const CliLayer = Layer.mergeAll(
75
88
  ReadinessService.Default,
76
89
  GraphMutationService.Default,
77
90
  DoctorService.Default,
91
+ ReviewService.Default,
78
92
  )
79
93
 
80
94
  /**
@@ -449,6 +463,9 @@ const commands: Record<string, Command> = {
449
463
  clearDescription: options["clear-description"],
450
464
  epic: options.epic,
451
465
  repo: options.repo?.[0],
466
+ review: options.review,
467
+ pullRequest: options["pull-request"],
468
+ ref: options.ref,
452
469
  references: options.reference,
453
470
  branch: options.branch,
454
471
  base: options.base,
@@ -477,6 +494,22 @@ const commands: Record<string, Command> = {
477
494
  )
478
495
  },
479
496
  },
497
+ review: {
498
+ run: async (args: string[], options: Record<string, any>) => {
499
+ if (options.help) return console.log(reviewHelp)
500
+ await runCommand(
501
+ review({
502
+ subcommand: args[0],
503
+ taskId: args[1],
504
+ ifRevision: options["if-revision"],
505
+ json: options.json,
506
+ silent: options.silent,
507
+ verbose: options.verbose,
508
+ cwd: options.cwd,
509
+ }),
510
+ )
511
+ },
512
+ },
480
513
  work: {
481
514
  run: async (args: string[], options: Record<string, any>) => {
482
515
  if (options.help) {
@@ -525,6 +558,26 @@ const commands: Record<string, Command> = {
525
558
  )
526
559
  },
527
560
  },
561
+ vcs: {
562
+ run: async (args: string[], options: Record<string, any>) => {
563
+ if (options.help) {
564
+ console.log(vcsHelp)
565
+ return
566
+ }
567
+ await runCommand(
568
+ vcs({
569
+ subcommand: args[0],
570
+ target: args[1],
571
+ apply: options.apply,
572
+ dryRun: options["dry-run"],
573
+ json: options.json,
574
+ silent: options.silent,
575
+ verbose: options.verbose,
576
+ cwd: options.cwd,
577
+ }),
578
+ )
579
+ },
580
+ },
528
581
  next: {
529
582
  run: async (_args: string[], options: Record<string, any>) => {
530
583
  if (options.help) return console.log(nextHelp)
@@ -680,9 +733,11 @@ Commands:
680
733
  archive <type> Archive a work item
681
734
  task <subcommand> Manage tasks
682
735
  work [directory|task] Work on an epic, task, or phase
683
- worktree <subcommand> Inspect and maintain managed worktrees
736
+ worktree <subcommand> Inspect and maintain managed workspaces
737
+ vcs <subcommand> Inspect or migrate the version-control backend
684
738
  next List or select ready execution units
685
739
  pr create Create a pull request for an execution unit
740
+ review refresh Explicitly refresh a pinned review task
686
741
  repo <subcommand> Manage workbase repositories
687
742
  status Show status for the current workbase
688
743
  doctor Diagnose workbase health and integrations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.49.0",
3
+ "version": "2.51.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -22,6 +22,7 @@
22
22
  "required": ["version"],
23
23
  "properties": {
24
24
  "version": { "const": 2 },
25
+ "vcs": { "enum": ["git", "jj"] },
25
26
  "root": { "type": "string" }
26
27
  }
27
28
  },
@@ -176,6 +177,7 @@
176
177
  { "$ref": "#/$defs/epicData" },
177
178
  { "$ref": "#/$defs/singleTaskData" },
178
179
  { "$ref": "#/$defs/multiTaskData" },
180
+ { "$ref": "#/$defs/reviewTaskData" },
179
181
  { "$ref": "#/$defs/phaseData" },
180
182
  { "$ref": "#/$defs/executionData" },
181
183
  { "$ref": "#/$defs/repositoryData" }
@@ -220,7 +222,8 @@
220
222
  "data": {
221
223
  "oneOf": [
222
224
  { "$ref": "#/$defs/singleTaskData" },
223
- { "$ref": "#/$defs/multiTaskData" }
225
+ { "$ref": "#/$defs/multiTaskData" },
226
+ { "$ref": "#/$defs/reviewTaskData" }
224
227
  ]
225
228
  },
226
229
  "workspace": { "$ref": "#/$defs/documentWorkspace" },
@@ -358,34 +361,72 @@
358
361
  "sha256": { "type": "string" }
359
362
  }
360
363
  },
361
- "phaseData": {
364
+ "reviewSource": {
365
+ "oneOf": [
366
+ {
367
+ "type": "object",
368
+ "additionalProperties": false,
369
+ "required": [
370
+ "kind",
371
+ "provider",
372
+ "repository",
373
+ "identifier",
374
+ "url",
375
+ "fetchRef"
376
+ ],
377
+ "properties": {
378
+ "kind": { "const": "pull-request" },
379
+ "provider": { "const": "github" },
380
+ "repository": { "type": "string" },
381
+ "identifier": { "type": "string", "pattern": "^\\d+$" },
382
+ "url": {
383
+ "type": "string",
384
+ "pattern": "^https://github\\.com/[^/]+/[^/]+/pull/\\d+$"
385
+ },
386
+ "fetchRef": { "type": "string", "pattern": "^refs/pull/\\d+/head$" }
387
+ }
388
+ },
389
+ {
390
+ "type": "object",
391
+ "additionalProperties": false,
392
+ "required": ["kind", "ref"],
393
+ "properties": {
394
+ "kind": { "const": "branch" },
395
+ "ref": { "type": "string", "pattern": "^refs/heads/" }
396
+ }
397
+ }
398
+ ]
399
+ },
400
+ "reviewRecord": {
362
401
  "type": "object",
363
402
  "additionalProperties": false,
364
- "required": ["repo", "branch", "base", "pr", "status", "sha256"],
403
+ "required": ["repo", "source", "commit", "refreshedAt"],
365
404
  "properties": {
366
- "description": { "type": "string" },
367
405
  "repo": { "type": "string" },
368
- "repos": {
369
- "type": "array",
370
- "items": { "$ref": "#/$defs/repositoryReference" }
371
- },
372
- "branch": { "type": "string" },
373
- "base": { "type": "string" },
374
- "pr": { "type": ["string", "null"] },
406
+ "source": { "$ref": "#/$defs/reviewSource" },
407
+ "commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
408
+ "refreshedAt": { "type": "string", "format": "date-time" }
409
+ }
410
+ },
411
+ "reviewTaskData": {
412
+ "type": "object",
413
+ "additionalProperties": false,
414
+ "required": ["ticketUrl", "review", "status", "sha256"],
415
+ "properties": {
416
+ "ticketUrl": { "type": ["string", "null"] },
417
+ "description": { "type": "string" },
418
+ "epic": { "type": "string" },
419
+ "review": { "$ref": "#/$defs/reviewRecord" },
375
420
  "status": { "$ref": "#/$defs/status" },
376
421
  "claim": { "$ref": "#/$defs/claim" },
377
422
  "sha256": { "type": "string" }
378
423
  }
379
424
  },
380
- "executionData": {
425
+ "phaseData": {
381
426
  "type": "object",
382
427
  "additionalProperties": false,
383
- "required": ["taskId", "repo", "branch", "base", "pr", "status"],
428
+ "required": ["repo", "branch", "base", "pr", "status", "sha256"],
384
429
  "properties": {
385
- "taskId": { "type": "string" },
386
- "phaseId": { "type": "string" },
387
- "ticketUrl": { "type": ["string", "null"] },
388
- "epic": { "type": "string" },
389
430
  "description": { "type": "string" },
390
431
  "repo": { "type": "string" },
391
432
  "repos": {
@@ -396,9 +437,50 @@
396
437
  "base": { "type": "string" },
397
438
  "pr": { "type": ["string", "null"] },
398
439
  "status": { "$ref": "#/$defs/status" },
399
- "claim": { "$ref": "#/$defs/claim" }
440
+ "claim": { "$ref": "#/$defs/claim" },
441
+ "sha256": { "type": "string" }
400
442
  }
401
443
  },
444
+ "executionData": {
445
+ "oneOf": [
446
+ {
447
+ "type": "object",
448
+ "additionalProperties": false,
449
+ "required": ["taskId", "repo", "branch", "base", "pr", "status"],
450
+ "properties": {
451
+ "taskId": { "type": "string" },
452
+ "phaseId": { "type": "string" },
453
+ "ticketUrl": { "type": ["string", "null"] },
454
+ "epic": { "type": "string" },
455
+ "description": { "type": "string" },
456
+ "repo": { "type": "string" },
457
+ "repos": {
458
+ "type": "array",
459
+ "items": { "$ref": "#/$defs/repositoryReference" }
460
+ },
461
+ "branch": { "type": "string" },
462
+ "base": { "type": "string" },
463
+ "pr": { "type": ["string", "null"] },
464
+ "status": { "$ref": "#/$defs/status" },
465
+ "claim": { "$ref": "#/$defs/claim" }
466
+ }
467
+ },
468
+ {
469
+ "type": "object",
470
+ "additionalProperties": false,
471
+ "required": ["taskId", "ticketUrl", "review", "status"],
472
+ "properties": {
473
+ "taskId": { "type": "string" },
474
+ "ticketUrl": { "type": ["string", "null"] },
475
+ "description": { "type": "string" },
476
+ "epic": { "type": "string" },
477
+ "review": { "$ref": "#/$defs/reviewRecord" },
478
+ "status": { "$ref": "#/$defs/status" },
479
+ "claim": { "$ref": "#/$defs/claim" }
480
+ }
481
+ }
482
+ ]
483
+ },
402
484
  "claim": {
403
485
  "type": "object",
404
486
  "additionalProperties": false,
@@ -469,26 +551,48 @@
469
551
  }
470
552
  },
471
553
  "executionGit": {
472
- "type": "object",
473
- "additionalProperties": false,
474
- "required": [
475
- "branch",
476
- "base",
477
- "branchCommit",
478
- "baseCommit",
479
- "checkoutCommit",
480
- "checkoutBranch",
481
- "dirty"
482
- ],
483
- "properties": {
484
- "branch": { "type": "string" },
485
- "base": { "type": "string" },
486
- "branchCommit": { "type": ["string", "null"] },
487
- "baseCommit": { "type": ["string", "null"] },
488
- "checkoutCommit": { "type": ["string", "null"] },
489
- "checkoutBranch": { "type": ["string", "null"] },
490
- "dirty": { "type": ["boolean", "null"] }
491
- }
554
+ "oneOf": [
555
+ {
556
+ "type": "object",
557
+ "additionalProperties": false,
558
+ "required": [
559
+ "branch",
560
+ "base",
561
+ "branchCommit",
562
+ "baseCommit",
563
+ "checkoutCommit",
564
+ "checkoutBranch",
565
+ "dirty"
566
+ ],
567
+ "properties": {
568
+ "branch": { "type": "string" },
569
+ "base": { "type": "string" },
570
+ "branchCommit": { "type": ["string", "null"] },
571
+ "baseCommit": { "type": ["string", "null"] },
572
+ "checkoutCommit": { "type": ["string", "null"] },
573
+ "checkoutBranch": { "type": ["string", "null"] },
574
+ "dirty": { "type": ["boolean", "null"] }
575
+ }
576
+ },
577
+ {
578
+ "type": "object",
579
+ "additionalProperties": false,
580
+ "required": [
581
+ "pinnedCommit",
582
+ "sourceCommit",
583
+ "checkoutCommit",
584
+ "checkoutBranch",
585
+ "dirty"
586
+ ],
587
+ "properties": {
588
+ "pinnedCommit": { "type": "string" },
589
+ "sourceCommit": { "type": ["string", "null"] },
590
+ "checkoutCommit": { "type": ["string", "null"] },
591
+ "checkoutBranch": { "type": ["string", "null"] },
592
+ "dirty": { "type": ["boolean", "null"] }
593
+ }
594
+ }
595
+ ]
492
596
  },
493
597
  "pr": {
494
598
  "oneOf": [
@@ -7,6 +7,68 @@ const expectUsageError = (args: string[], usage: string) => {
7
7
  }
8
8
 
9
9
  describe("strict CLI parsing", () => {
10
+ test("accepts review creation and refresh while enforcing one source", () => {
11
+ expect(
12
+ parseCli([
13
+ "task",
14
+ "create",
15
+ "review-it",
16
+ "--review",
17
+ "agency",
18
+ "--pull-request",
19
+ "42",
20
+ ]).values,
21
+ ).toMatchObject({ review: "agency", "pull-request": "42" })
22
+ expect(() =>
23
+ parseCli([
24
+ "task",
25
+ "create",
26
+ "review-it",
27
+ "--review",
28
+ "agency",
29
+ "--ref",
30
+ "feature/review",
31
+ ]),
32
+ ).not.toThrow()
33
+ expect(() =>
34
+ parseCli([
35
+ "review",
36
+ "refresh",
37
+ "review-it",
38
+ "--if-revision",
39
+ "a".repeat(64),
40
+ ]),
41
+ ).not.toThrow()
42
+ expect(() =>
43
+ parseCli([
44
+ "task",
45
+ "create",
46
+ "review-it",
47
+ "--review",
48
+ "agency",
49
+ "--pull-request",
50
+ "42",
51
+ "--ref",
52
+ "feature/review",
53
+ ]),
54
+ ).toThrow("exactly one")
55
+ for (const extra of [
56
+ ["--ref", "feature/review", "--repo", "agency"],
57
+ ["--ref", "feature/review", "--multi-phase"],
58
+ ]) {
59
+ expect(() =>
60
+ parseCli([
61
+ "task",
62
+ "create",
63
+ "review-it",
64
+ "--review",
65
+ "agency",
66
+ ...extra,
67
+ ]),
68
+ ).toThrow("cannot be combined")
69
+ }
70
+ })
71
+
10
72
  test("accepts every documented orchestration recipe command", () => {
11
73
  for (const args of orchestrationRecipes) {
12
74
  expect(() => parseCli(args)).not.toThrow()
@@ -509,6 +571,25 @@ describe("strict CLI parsing", () => {
509
571
  )
510
572
  })
511
573
 
574
+ test("parses VCS status and migration commands", () => {
575
+ expect(parseCli(["vcs", "status", "--json"])).toMatchObject({
576
+ commandName: "vcs",
577
+ args: ["status"],
578
+ values: { json: true },
579
+ })
580
+ expect(
581
+ parseCli(["vcs", "migrate", "jj", "--apply", "--json"]),
582
+ ).toMatchObject({
583
+ commandName: "vcs",
584
+ args: ["migrate", "jj"],
585
+ values: { apply: true, json: true },
586
+ })
587
+ expectUsageError(["vcs", "migrate"], "agency vcs migrate")
588
+ expect(() =>
589
+ parseCli(["vcs", "migrate", "jj", "--dry-run", "--apply"]),
590
+ ).toThrow("cannot be combined")
591
+ })
592
+
512
593
  test("accepts runner selection and command inspection for work", () => {
513
594
  expect(
514
595
  parseCli([