@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,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { Effect } from "effect"
3
- import { mkdir, rm } from "node:fs/promises"
3
+ import { mkdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
6
  import { ArchiveService } from "./ArchiveService"
@@ -22,17 +22,6 @@ const git = async (args: string[], cwd?: string) => {
22
22
  }
23
23
  }
24
24
 
25
- const jj = async (args: string[]) => {
26
- const process = Bun.spawn(["jj", ...args], {
27
- stdout: "pipe",
28
- stderr: "pipe",
29
- })
30
- await process.exited
31
- if (process.exitCode !== 0) {
32
- throw new Error(await new Response(process.stderr).text())
33
- }
34
- }
35
-
36
25
  describe("ArchiveService", () => {
37
26
  let root: string
38
27
  const dropTask = (id: string) =>
@@ -276,166 +265,6 @@ describe("ArchiveService", () => {
276
265
  ).toBe(0)
277
266
  })
278
267
 
279
- test("retries archive after a jj workspace was forgotten but not deleted", async () => {
280
- if (!Bun.which("jj")) return
281
- const repository = join(root, "repos/agency")
282
- await rm(repository, { recursive: true, force: true })
283
- await git(["clone", join(root, "source"), repository])
284
- await jj(["git", "init", "--colocate", repository])
285
- await Bun.write(
286
- join(root, "agency.json"),
287
- JSON.stringify({ version: 2, vcs: "jj" }),
288
- )
289
- await runTestEffect(
290
- TaskService.pipe(
291
- Effect.flatMap((service) =>
292
- service.create(
293
- {
294
- id: "interrupted",
295
- ticketUrl: null,
296
- repo: "agency",
297
- branch: "task/interrupted",
298
- base: "main",
299
- },
300
- root,
301
- ),
302
- ),
303
- ),
304
- )
305
- await runTestEffect(
306
- TaskService.pipe(
307
- Effect.flatMap((service) =>
308
- service.setStatus("interrupted", "dropped", root),
309
- ),
310
- ),
311
- )
312
- const workspace = await runTestEffect(
313
- WorktreeService.pipe(
314
- Effect.flatMap((service) =>
315
- service.materialize("interrupted", undefined, root),
316
- ),
317
- ),
318
- )
319
- await jj([
320
- "-R",
321
- repository,
322
- "workspace",
323
- "forget",
324
- "agency-interrupted-task-agency",
325
- ])
326
-
327
- const preview = await runTestEffect(
328
- ArchiveService.pipe(
329
- Effect.flatMap((service) =>
330
- service.archiveTask("interrupted", root, { dryRun: true }),
331
- ),
332
- ),
333
- )
334
- expect(preview.removedWorktrees).toEqual([workspace.writablePath!])
335
-
336
- await runTestEffect(
337
- ArchiveService.pipe(
338
- Effect.flatMap((service) => service.archiveTask("interrupted", root)),
339
- ),
340
- )
341
- expect(await Bun.file(workspace.writablePath!).exists()).toBe(false)
342
- expect(
343
- await Bun.file(join(root, "archive/tasks/interrupted/TASK.md")).exists(),
344
- ).toBe(true)
345
- })
346
-
347
- test("refuses a modified residual jj checkout after workspace removal", async () => {
348
- if (!Bun.which("jj")) return
349
- const repository = join(root, "repos/agency")
350
- await rm(repository, { recursive: true, force: true })
351
- await git(["clone", join(root, "source"), repository])
352
- await jj(["git", "init", "--colocate", repository])
353
- await Bun.write(
354
- join(root, "agency.json"),
355
- JSON.stringify({ version: 2, vcs: "jj" }),
356
- )
357
- await runTestEffect(
358
- TaskService.pipe(
359
- Effect.flatMap((service) =>
360
- service.create(
361
- {
362
- id: "modified-residual",
363
- ticketUrl: null,
364
- repo: "agency",
365
- branch: "task/modified-residual",
366
- base: "main",
367
- },
368
- root,
369
- ),
370
- ),
371
- ),
372
- )
373
- await runTestEffect(
374
- TaskService.pipe(
375
- Effect.flatMap((service) =>
376
- service.setStatus("modified-residual", "dropped", root),
377
- ),
378
- ),
379
- )
380
- const workspace = await runTestEffect(
381
- WorktreeService.pipe(
382
- Effect.flatMap((service) =>
383
- service.materialize("modified-residual", undefined, root),
384
- ),
385
- ),
386
- )
387
- await jj([
388
- "-R",
389
- repository,
390
- "workspace",
391
- "forget",
392
- "agency-modified-residual-task-agency",
393
- ])
394
- await Bun.write(join(workspace.writablePath!, "keep.txt"), "keep me\n")
395
-
396
- await expect(
397
- runTestEffect(
398
- ArchiveService.pipe(
399
- Effect.flatMap((service) =>
400
- service.archiveTask("modified-residual", root),
401
- ),
402
- ),
403
- ),
404
- ).rejects.toThrow("contents do not match expected revision")
405
- expect(
406
- await Bun.file(join(workspace.writablePath!, "keep.txt")).text(),
407
- ).toBe("keep me\n")
408
- expect(
409
- await Bun.file(join(root, "tasks/modified-residual/TASK.md")).exists(),
410
- ).toBe(true)
411
- expect(
412
- await Bun.file(
413
- join(
414
- root,
415
- `.agency-worktree-${Buffer.from("modified-residual:task").toString("hex")}.lock`,
416
- ),
417
- ).exists(),
418
- ).toBe(false)
419
-
420
- await rm(join(workspace.writablePath!, "keep.txt"))
421
- await rm(join(workspace.writablePath!, ".jj"), {
422
- recursive: true,
423
- force: true,
424
- })
425
- await expect(
426
- runTestEffect(
427
- ArchiveService.pipe(
428
- Effect.flatMap((service) =>
429
- service.archiveTask("modified-residual", root),
430
- ),
431
- ),
432
- ),
433
- ).rejects.toThrow("is not registered as a jj workspace")
434
- expect(
435
- await Bun.file(join(workspace.writablePath!, "README.md")).exists(),
436
- ).toBe(true)
437
- })
438
-
439
268
  test("archives a phase in the mirrored task hierarchy", async () => {
440
269
  await runTestEffect(
441
270
  TaskService.pipe(
@@ -1,7 +1,7 @@
1
1
  import { Schema, TreeFormatter } from "@effect/schema"
2
2
  import { Data, Effect, Either, Layer } from "effect"
3
3
  import { lstat, mkdir, open, rename, rm } from "node:fs/promises"
4
- import { dirname, join, relative } from "node:path"
4
+ import { dirname, join, relative, resolve, sep } from "node:path"
5
5
  import { EpicService, type EpicRecord } from "./EpicService"
6
6
  import { FileSystemService } from "./FileSystemService"
7
7
  import { PhaseService, type PhaseRecord } from "./PhaseService"
@@ -13,7 +13,6 @@ import {
13
13
  } from "./WorktreeService"
14
14
  import {
15
15
  GitVersionControlService,
16
- JjVersionControlService,
17
16
  VersionControlService,
18
17
  } from "./VersionControlService"
19
18
  import {
@@ -53,6 +52,11 @@ class ArchiveError extends Data.TaggedError("ArchiveError")<{
53
52
 
54
53
  export type ArchiveKind = "epic" | "task" | "phase"
55
54
 
55
+ interface ArchivePathTarget {
56
+ readonly kind: "epic" | "task"
57
+ readonly id: string
58
+ }
59
+
56
60
  const LifecycleEventSchema = Schema.Struct({
57
61
  operation: Schema.Literal("archive", "restore"),
58
62
  at: Schema.String,
@@ -368,7 +372,6 @@ const WorktreeLayer = Layer.mergeAll(
368
372
  FileSystemService.Default,
369
373
  WorkbaseService.Default,
370
374
  GitVersionControlService.Default,
371
- JjVersionControlService.Default,
372
375
  VersionControlService.Default,
373
376
  TaskService.Default,
374
377
  PhaseService.Default,
@@ -400,22 +403,6 @@ const restoreWorktreeSnapshots = async (
400
403
  continue
401
404
  } catch {}
402
405
  await mkdir(dirname(snapshot.path), { recursive: true })
403
- if (snapshot.vcs === "jj") {
404
- await runGit([
405
- "jj",
406
- "-R",
407
- snapshot.repositoryPath,
408
- "workspace",
409
- "add",
410
- "--name",
411
- snapshot.workspaceName!,
412
- "-r",
413
- snapshot.head,
414
- snapshot.path,
415
- ])
416
- await runGit(["jj", "-R", snapshot.path, "edit", snapshot.head])
417
- continue
418
- }
419
406
  await runGit(
420
407
  snapshot.branch
421
408
  ? [
@@ -496,6 +483,59 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
496
483
  "ArchiveService",
497
484
  {
498
485
  sync: () => ({
486
+ resolvePathTarget: (path: string, cwd: string = process.cwd()) =>
487
+ Effect.gen(function* () {
488
+ const fs = yield* FileSystemService
489
+ const workbase = yield* WorkbaseService
490
+ const candidate = resolve(cwd, path)
491
+ if (!(yield* fs.exists(candidate))) {
492
+ return yield* new ArchiveError({
493
+ message: `Archive path does not exist: ${candidate}`,
494
+ })
495
+ }
496
+
497
+ const canonicalPath = yield* fs.realPath(candidate)
498
+ const root = yield* workbase.discover(canonicalPath)
499
+ const child = relative(root, canonicalPath)
500
+ const parts = child.split(sep)
501
+ if (child === "" || parts.length === 1) {
502
+ return yield* new ArchiveError({
503
+ message: `Archive path is ambiguous; it does not identify a single epic or task: ${canonicalPath}`,
504
+ })
505
+ }
506
+
507
+ const [collection, id] = parts
508
+ const kind =
509
+ collection === "epics"
510
+ ? "epic"
511
+ : collection === "tasks"
512
+ ? "task"
513
+ : undefined
514
+ if (!kind || !id) {
515
+ return yield* new ArchiveError({
516
+ message: `Archive path must be within an active epic or task: ${canonicalPath}`,
517
+ })
518
+ }
519
+ if (kind === "task" && parts[2] === "phases") {
520
+ return yield* new ArchiveError({
521
+ message: `Archive path identifies a phase; use 'agency archive phase <task-id> <phase-id>': ${canonicalPath}`,
522
+ })
523
+ }
524
+
525
+ const document = join(
526
+ root,
527
+ kind === "epic" ? "epics" : "tasks",
528
+ id,
529
+ kind === "epic" ? "EPIC.md" : "TASK.md",
530
+ )
531
+ if (!(yield* fs.exists(document))) {
532
+ return yield* new ArchiveError({
533
+ message: `Archive path does not identify an active ${kind}: ${canonicalPath}`,
534
+ })
535
+ }
536
+ return { kind, id } satisfies ArchivePathTarget
537
+ }),
538
+
499
539
  list: (filters: ArchiveFilters = {}, startPath: string = process.cwd()) =>
500
540
  Effect.gen(function* () {
501
541
  const fs = yield* FileSystemService
@@ -724,7 +764,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
724
764
  removedWorktrees.push(
725
765
  ...(yield* worktrees.remove(unit.taskId, unit.phaseId, root, {
726
766
  dryRun: true,
727
- persistResume: false,
728
767
  })),
729
768
  )
730
769
  }
@@ -783,7 +822,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
783
822
  worktrees.remove(unit.taskId, unit.phaseId, root, {
784
823
  snapshots,
785
824
  lockHeld: true,
786
- persistResume: false,
787
825
  }),
788
826
  )
789
827
  } catch (cause) {
@@ -897,7 +935,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
897
935
  const result = yield* worktrees
898
936
  .remove(unit.taskId, executionPhaseId(unit), root, {
899
937
  dryRun: true,
900
- persistResume: false,
901
938
  task: context.task,
902
939
  phase: unit.phaseId
903
940
  ? context.phases.find((phase) => phase.id === unit.phaseId)
@@ -1075,7 +1112,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
1075
1112
  worktrees.remove(unit.taskId, unit.phaseId, root, {
1076
1113
  snapshots,
1077
1114
  lockHeld: true,
1078
- persistResume: false,
1079
1115
  task: contexts.get(unit.taskId)!.task,
1080
1116
  phase: unit.phaseId
1081
1117
  ? contexts
@@ -1232,7 +1268,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
1232
1268
  root,
1233
1269
  {
1234
1270
  dryRun: true,
1235
- persistResume: false,
1236
1271
  },
1237
1272
  )),
1238
1273
  )
@@ -1288,7 +1323,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
1288
1323
  {
1289
1324
  snapshots,
1290
1325
  lockHeld: true,
1291
- persistResume: false,
1292
1326
  },
1293
1327
  ),
1294
1328
  )
@@ -1381,7 +1415,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
1381
1415
  yield* rejectExistingDestination(destination, "Archive")
1382
1416
  const removedWorktrees = yield* worktrees.remove(taskId, id, root, {
1383
1417
  dryRun: true,
1384
- persistResume: false,
1385
1418
  })
1386
1419
  const at = new Date().toISOString()
1387
1420
  const content = yield* declarationContent(task, {
@@ -1430,7 +1463,6 @@ export class ArchiveService extends Effect.Service<ArchiveService>()(
1430
1463
  worktrees.remove(taskId, id, root, {
1431
1464
  snapshots,
1432
1465
  lockHeld: true,
1433
- persistResume: false,
1434
1466
  }),
1435
1467
  )
1436
1468
  },
@@ -350,7 +350,7 @@ export class ContextService extends Effect.Service<ContextService>()(
350
350
  root,
351
351
  configPath: join(root, "agency.json"),
352
352
  version: config.version,
353
- vcs: config.vcs ?? "git",
353
+ vcs: "git",
354
354
  },
355
355
  target: { kind: "workbase", path: root },
356
356
  hint: compact
@@ -1025,51 +1025,9 @@ export class ContextService extends Effect.Service<ContextService>()(
1025
1025
  const inspectCheckout = (
1026
1026
  repositoryPath: string,
1027
1027
  checkoutPath: string,
1028
- expectedBranch: string | null = null,
1029
- branchCommit: string | null = null,
1030
1028
  ) =>
1031
1029
  Effect.gen(function* (): Generator<any, CheckoutInspection, any> {
1032
1030
  const materialized = yield* fs.isDirectory(checkoutPath)
1033
- if (backend.kind === "jj") {
1034
- const listed = yield* Effect.either(
1035
- backend.listWorkspaces(repositoryPath),
1036
- )
1037
- const workspaces = Either.isRight(listed) ? listed.right : []
1038
- if (Either.isLeft(listed)) {
1039
- inspectionWarnings.push(
1040
- `Unable to inspect jj workspace registrations for ${repositoryPath}`,
1041
- )
1042
- }
1043
- const canonicalCheckoutPath = materialized
1044
- ? yield* fs.realPath(checkoutPath)
1045
- : resolve(checkoutPath)
1046
- const workspace = workspaces.find(
1047
- (workspace) => workspace.path === canonicalCheckoutPath,
1048
- )
1049
- const registered = workspace !== undefined
1050
- if (!materialized) {
1051
- return {
1052
- materialized: false,
1053
- registered,
1054
- checkoutCommit: null,
1055
- checkoutBranch: null,
1056
- detached: null,
1057
- dirty: null,
1058
- }
1059
- }
1060
- return {
1061
- materialized: true,
1062
- registered,
1063
- checkoutCommit: workspace?.commit ?? null,
1064
- checkoutBranch:
1065
- expectedBranch && branchCommit === workspace?.commit
1066
- ? expectedBranch
1067
- : null,
1068
- detached:
1069
- !expectedBranch || branchCommit !== workspace?.commit,
1070
- dirty: workspace ? false : null,
1071
- }
1072
- }
1073
1031
  const listed = yield* runGit(fs, repositoryPath, [
1074
1032
  "worktree",
1075
1033
  "list",
@@ -1136,7 +1094,7 @@ export class ContextService extends Effect.Service<ContextService>()(
1136
1094
  const repositoryPath =
1137
1095
  repository?.path ?? join(root, "repos", executionData.repo)
1138
1096
  const checkoutPath = join(codePath, executionData.repo)
1139
- let branchCommit = yield* backend.resolveRevision(
1097
+ const branchCommit = yield* backend.resolveRevision(
1140
1098
  repositoryPath,
1141
1099
  executionData.branch,
1142
1100
  )
@@ -1147,8 +1105,6 @@ export class ContextService extends Effect.Service<ContextService>()(
1147
1105
  const checkout = yield* inspectCheckout(
1148
1106
  repositoryPath,
1149
1107
  checkoutPath,
1150
- executionData.branch,
1151
- branchCommit,
1152
1108
  )
1153
1109
  if (branchCommit === null) {
1154
1110
  inspectionWarnings.push(
@@ -1261,7 +1217,7 @@ export class ContextService extends Effect.Service<ContextService>()(
1261
1217
  root,
1262
1218
  configPath: join(root, "agency.json"),
1263
1219
  version: config.version,
1264
- vcs: config.vcs ?? "git",
1220
+ vcs: "git",
1265
1221
  },
1266
1222
  target,
1267
1223
  documents: {
@@ -125,20 +125,12 @@ export class DoctorService extends Effect.Service<DoctorService>()(
125
125
  return available
126
126
  })
127
127
 
128
- const [gitAvailable, jjAvailable] = yield* Effect.all(
129
- [
130
- tool("tool.git", "git", "error", "Git"),
131
- tool(
132
- "tool.jj",
133
- "jj",
134
- config.vcs === "jj" ? "error" : "optional",
135
- "Jujutsu",
136
- ),
137
- ],
138
- { concurrency: "unbounded", batching: true },
128
+ const versionControlAvailable = yield* tool(
129
+ "tool.git",
130
+ "git",
131
+ "error",
132
+ "Git",
139
133
  )
140
- const versionControlAvailable =
141
- gitAvailable && (config.vcs !== "jj" || jjAvailable)
142
134
  yield* Effect.all(
143
135
  [
144
136
  tool(
@@ -180,15 +172,6 @@ export class DoctorService extends Effect.Service<DoctorService>()(
180
172
  ] as const,
181
173
  ]
182
174
  : []),
183
- ...(config.workspaceCreateCommand
184
- ? [
185
- [
186
- "integration.workspace-create",
187
- config.workspaceCreateCommand,
188
- "Workspace creator",
189
- ] as const,
190
- ]
191
- : []),
192
175
  ...Object.entries(config.repositories ?? {}).flatMap(
193
176
  ([alias, repository]) =>
194
177
  repository.postCheckoutCommand
@@ -384,8 +367,9 @@ export class DoctorService extends Effect.Service<DoctorService>()(
384
367
  category: "repository",
385
368
  level: "warning",
386
369
  status: "fail",
387
- message: `Repository, ref, remote, and workspace checks were skipped because the ${config.vcs ?? "git"} backend is unavailable`,
388
- remediation: `Install '${config.vcs === "jj" ? "jj" : "git"}' and rerun 'agency doctor'.`,
370
+ message:
371
+ "Repository, ref, remote, and workspace checks were skipped because Git is unavailable",
372
+ remediation: "Install 'git' and rerun 'agency doctor'.",
389
373
  })
390
374
  }
391
375
  for (const repository of repositoryList) {
@@ -121,6 +121,7 @@ describe("GraphService", () => {
121
121
  const graph = await getGraph(root)
122
122
 
123
123
  expect(Schema.decodeUnknownSync(AgencyGraph)(graph)).toEqual(graph)
124
+ expect(graph.workbase.vcs).toBe("git")
124
125
  expect(graph.nodes.map((node) => node.id)).toEqual([
125
126
  "epic:delivery",
126
127
  "execution-unit:phase/ship/implement",
@@ -821,10 +821,7 @@ export class GraphService extends Effect.Service<GraphService>()(
821
821
  checkoutCommit: materialized
822
822
  ? yield* backend.workspaceHead(checkoutPath)
823
823
  : null,
824
- checkoutBranch:
825
- backend.kind === "jj" && materialized
826
- ? data.branch
827
- : actualBranch,
824
+ checkoutBranch: actualBranch,
828
825
  dirty: materialized
829
826
  ? yield* backend.workspaceDirty(checkoutPath)
830
827
  : null,
@@ -1022,7 +1019,7 @@ export class GraphService extends Effect.Service<GraphService>()(
1022
1019
  version: GRAPH_VERSION,
1023
1020
  workbase: {
1024
1021
  version: config.version,
1025
- vcs: config.vcs ?? "git",
1022
+ vcs: "git",
1026
1023
  ...(include.has("workspace") ? { root } : {}),
1027
1024
  },
1028
1025
  filters,