@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
@@ -8,6 +8,7 @@ import { RepositoryService } from "./RepositoryService"
8
8
  import { aggregateProgress, readinessState } from "../readiness"
9
9
  import { parseFrontmatter } from "../workbase/frontmatter"
10
10
  import { documentRevision } from "../workbase/document-revision"
11
+ import { VersionControlService } from "./VersionControlService"
11
12
  import {
12
13
  EpicFrontmatter,
13
14
  PhaseFrontmatter,
@@ -54,6 +55,7 @@ interface CheckoutInspection {
54
55
  readonly checkoutCommit: string | null
55
56
  readonly checkoutBranch: string | null
56
57
  readonly detached: boolean | null
58
+ readonly dirty: boolean | null
57
59
  }
58
60
 
59
61
  type ExecutionData = PhaseData & Partial<Pick<TaskData, "ticketUrl" | "epic">>
@@ -92,6 +94,18 @@ const runGit = (fs: FileSystemService, cwd: string, args: readonly string[]) =>
92
94
  Effect.catchAll(() => Effect.succeed(null)),
93
95
  )
94
96
 
97
+ const runGitText = (
98
+ fs: FileSystemService,
99
+ cwd: string,
100
+ args: readonly string[],
101
+ ) =>
102
+ fs.runCommand(["git", "-C", cwd, ...args], { captureOutput: true }).pipe(
103
+ Effect.map((result) =>
104
+ result.exitCode === 0 ? result.stdout.trim() : null,
105
+ ),
106
+ Effect.catchAll(() => Effect.succeed(null)),
107
+ )
108
+
95
109
  const worktreePaths = (output: string | null) =>
96
110
  new Set(
97
111
  (output ?? "")
@@ -114,6 +128,7 @@ export class ContextService extends Effect.Service<ContextService>()(
114
128
  const fs = yield* FileSystemService
115
129
  const workbase = yield* WorkbaseService
116
130
  const repositoryService = yield* RepositoryService
131
+ const versionControl = yield* VersionControlService
117
132
  const cwd = resolve(options.cwd ?? process.cwd())
118
133
  const suppliedTarget = options.target ?? "."
119
134
  const candidate = resolve(cwd, suppliedTarget)
@@ -121,6 +136,7 @@ export class ContextService extends Effect.Service<ContextService>()(
121
136
  const { root, config } = yield* workbase.loadConfig(
122
137
  candidateExists ? candidate : cwd,
123
138
  )
139
+ const backend = yield* versionControl.forWorkbase(root)
124
140
 
125
141
  if (relative(root, candidate) === "") {
126
142
  const compact = !options.full
@@ -201,6 +217,7 @@ export class ContextService extends Effect.Service<ContextService>()(
201
217
  root,
202
218
  configPath: join(root, "agency.json"),
203
219
  version: config.version,
220
+ vcs: config.vcs ?? "git",
204
221
  },
205
222
  target: { kind: "workbase", path: root },
206
223
  hint: compact
@@ -765,9 +782,13 @@ export class ContextService extends Effect.Service<ContextService>()(
765
782
  : task?.data && "repo" in task.data
766
783
  ? (task.data as ExecutionData)
767
784
  : null
768
- const references: readonly RepositoryReference[] = executionData
769
- ? (executionData.repos ?? [])
770
- : (epic?.data.repos ?? [])
785
+ const reviewData =
786
+ task?.data && "review" in task.data ? task.data.review : null
787
+ const references: readonly RepositoryReference[] = reviewData
788
+ ? [{ repo: reviewData.repo, ref: reviewData.commit }]
789
+ : executionData
790
+ ? (executionData.repos ?? [])
791
+ : (epic?.data.repos ?? [])
771
792
  const entityDirectory = target.path.replace(
772
793
  /\/(?:EPIC|TASK|PHASE)\.md$/,
773
794
  "",
@@ -784,9 +805,45 @@ export class ContextService extends Effect.Service<ContextService>()(
784
805
  const inspectCheckout = (
785
806
  repositoryPath: string,
786
807
  checkoutPath: string,
808
+ expectedBranch: string | null = null,
787
809
  ) =>
788
810
  Effect.gen(function* (): Generator<any, CheckoutInspection, any> {
789
811
  const materialized = yield* fs.isDirectory(checkoutPath)
812
+ if (backend.kind === "jj") {
813
+ const listed = yield* Effect.either(
814
+ backend.listWorkspaces(repositoryPath),
815
+ )
816
+ const workspaces = Either.isRight(listed) ? listed.right : []
817
+ if (Either.isLeft(listed)) {
818
+ inspectionWarnings.push(
819
+ `Unable to inspect jj workspace registrations for ${repositoryPath}`,
820
+ )
821
+ }
822
+ const canonicalCheckoutPath = materialized
823
+ ? yield* fs.realPath(checkoutPath)
824
+ : resolve(checkoutPath)
825
+ const registered = workspaces.some(
826
+ (workspace) => workspace.path === canonicalCheckoutPath,
827
+ )
828
+ if (!materialized) {
829
+ return {
830
+ materialized: false,
831
+ registered,
832
+ checkoutCommit: null,
833
+ checkoutBranch: null,
834
+ detached: null,
835
+ dirty: null,
836
+ }
837
+ }
838
+ return {
839
+ materialized: true,
840
+ registered,
841
+ checkoutCommit: yield* backend.workspaceHead(checkoutPath),
842
+ checkoutBranch: expectedBranch,
843
+ detached: expectedBranch === null,
844
+ dirty: yield* backend.workspaceDirty(checkoutPath),
845
+ }
846
+ }
790
847
  const listed = yield* runGit(fs, repositoryPath, [
791
848
  "worktree",
792
849
  "list",
@@ -812,6 +869,7 @@ export class ContextService extends Effect.Service<ContextService>()(
812
869
  checkoutCommit: null,
813
870
  checkoutBranch: null,
814
871
  detached: null,
872
+ dirty: null,
815
873
  }
816
874
  }
817
875
  const checkoutCommit = yield* runGit(fs, checkoutPath, [
@@ -824,6 +882,10 @@ export class ContextService extends Effect.Service<ContextService>()(
824
882
  "--short",
825
883
  "HEAD",
826
884
  ])
885
+ const checkoutStatus = yield* runGitText(fs, checkoutPath, [
886
+ "status",
887
+ "--porcelain",
888
+ ])
827
889
  const resolvedCheckoutPath = yield* fs.realPath(checkoutPath)
828
890
  registered = registered || listedPaths.has(resolvedCheckoutPath)
829
891
  if (checkoutCommit === null) {
@@ -837,6 +899,8 @@ export class ContextService extends Effect.Service<ContextService>()(
837
899
  checkoutCommit,
838
900
  checkoutBranch,
839
901
  detached: checkoutBranch === null,
902
+ dirty:
903
+ checkoutStatus === null ? null : checkoutStatus.length > 0,
840
904
  }
841
905
  })
842
906
 
@@ -846,14 +910,22 @@ export class ContextService extends Effect.Service<ContextService>()(
846
910
  const repositoryPath =
847
911
  repository?.path ?? join(root, "repos", executionData.repo)
848
912
  const checkoutPath = join(codePath, executionData.repo)
849
- const branchCommit = yield* runGit(fs, repositoryPath, [
850
- "rev-parse",
851
- `${executionData.branch}^{commit}`,
852
- ])
853
- const baseCommit = yield* runGit(fs, repositoryPath, [
854
- "rev-parse",
855
- `${executionData.base}^{commit}`,
856
- ])
913
+ let branchCommit = yield* backend.resolveRevision(
914
+ repositoryPath,
915
+ executionData.branch,
916
+ )
917
+ const baseCommit = yield* backend.resolveRevision(
918
+ repositoryPath,
919
+ executionData.base,
920
+ )
921
+ const checkout = yield* inspectCheckout(
922
+ repositoryPath,
923
+ checkoutPath,
924
+ executionData.branch,
925
+ )
926
+ if (backend.kind === "jj" && branchCommit === null) {
927
+ branchCommit = checkout.checkoutCommit
928
+ }
857
929
  if (branchCommit === null) {
858
930
  inspectionWarnings.push(
859
931
  `Unable to resolve branch '${executionData.branch}' in ${repositoryPath}`,
@@ -872,7 +944,7 @@ export class ContextService extends Effect.Service<ContextService>()(
872
944
  base: executionData.base,
873
945
  branchCommit,
874
946
  baseCommit,
875
- ...(yield* inspectCheckout(repositoryPath, checkoutPath)),
947
+ ...checkout,
876
948
  }
877
949
  })
878
950
  : null
@@ -883,10 +955,10 @@ export class ContextService extends Effect.Service<ContextService>()(
883
955
  const repositoryPath =
884
956
  repository?.path ?? join(root, "repos", reference.repo)
885
957
  const checkoutPath = join(codePath, reference.repo)
886
- const resolvedCommit = yield* runGit(fs, repositoryPath, [
887
- "rev-parse",
888
- `${reference.ref}^{commit}`,
889
- ])
958
+ const resolvedCommit = yield* backend.resolveRevision(
959
+ repositoryPath,
960
+ reference.ref,
961
+ )
890
962
  if (resolvedCommit === null) {
891
963
  inspectionWarnings.push(
892
964
  `Unable to resolve reference '${reference.ref}' in ${repositoryPath}`,
@@ -901,6 +973,17 @@ export class ContextService extends Effect.Service<ContextService>()(
901
973
  ...(yield* inspectCheckout(repositoryPath, checkoutPath)),
902
974
  })
903
975
  }
976
+ const reviewSourceCommit = reviewData
977
+ ? yield* runGit(fs, join(root, "repos", reviewData.repo), [
978
+ "ls-remote",
979
+ "origin",
980
+ reviewData.source.kind === "pull-request"
981
+ ? reviewData.source.fetchRef
982
+ : reviewData.source.ref
983
+ .replace(/^refs\/remotes\/origin\//, "")
984
+ .replace(/^origin\//, ""),
985
+ ])
986
+ : null
904
987
 
905
988
  const checkoutStates = [writable, ...referenceCheckouts].filter(
906
989
  (value): value is NonNullable<typeof value> => value !== null,
@@ -933,6 +1016,7 @@ export class ContextService extends Effect.Service<ContextService>()(
933
1016
  root,
934
1017
  configPath: join(root, "agency.json"),
935
1018
  version: config.version,
1019
+ vcs: config.vcs ?? "git",
936
1020
  },
937
1021
  target,
938
1022
  documents: {
@@ -956,7 +1040,11 @@ export class ContextService extends Effect.Service<ContextService>()(
956
1040
  aggregate: aggregateProgress(aggregateStatuses),
957
1041
  },
958
1042
  authority: {
959
- mode: executionData ? "execution" : "orchestration",
1043
+ mode: reviewData
1044
+ ? "review"
1045
+ : executionData
1046
+ ? "execution"
1047
+ : "orchestration",
960
1048
  writable: writable
961
1049
  ? {
962
1050
  repo: writable.repo,
@@ -972,6 +1060,9 @@ export class ContextService extends Effect.Service<ContextService>()(
972
1060
  repositoryPath: reference.repositoryPath,
973
1061
  checkoutPath: reference.checkoutPath,
974
1062
  })),
1063
+ documents: {
1064
+ writable: reviewData && task ? [task.path] : [],
1065
+ },
975
1066
  },
976
1067
  workspace: options.compact
977
1068
  ? {
@@ -997,6 +1088,16 @@ export class ContextService extends Effect.Service<ContextService>()(
997
1088
  references: referenceCheckouts,
998
1089
  warnings: inspectionWarnings,
999
1090
  },
1091
+ review: reviewData
1092
+ ? {
1093
+ ...reviewData,
1094
+ sourceObservation: {
1095
+ available: reviewSourceCommit !== null,
1096
+ commit: reviewSourceCommit?.split(/\s+/)[0] ?? null,
1097
+ },
1098
+ checkout: referenceCheckouts[0] ?? null,
1099
+ }
1100
+ : null,
1000
1101
  pr: executionData?.pr
1001
1102
  ? normalizePullRequestRecord(executionData.pr)
1002
1103
  : { url: null, state: "none" },
@@ -126,6 +126,14 @@ export class DoctorService extends Effect.Service<DoctorService>()(
126
126
  }
127
127
 
128
128
  const gitAvailable = yield* tool("tool.git", "git", "error", "Git")
129
+ const jjAvailable = yield* tool(
130
+ "tool.jj",
131
+ "jj",
132
+ config.vcs === "jj" ? "error" : "optional",
133
+ "Jujutsu",
134
+ )
135
+ const versionControlAvailable =
136
+ gitAvailable && (config.vcs !== "jj" || jjAvailable)
129
137
  yield* tool(
130
138
  "capability.runner.opencode",
131
139
  "opencode",
@@ -282,6 +290,8 @@ export class DoctorService extends Effect.Service<DoctorService>()(
282
290
  }
283
291
 
284
292
  const refs = new Map<string, Set<string>>()
293
+ const reviewSources: { repo: string; task: string; ref: string }[] =
294
+ []
285
295
  const declareRef = (repo: string, ref: string) => {
286
296
  const values = refs.get(repo) ?? new Set<string>()
287
297
  values.add(ref)
@@ -293,7 +303,19 @@ export class DoctorService extends Effect.Service<DoctorService>()(
293
303
  declareRef(reference.repo, reference.ref)
294
304
  }
295
305
  for (const task of yield* tasks.list(root)) {
296
- if ("repo" in task.data) {
306
+ if ("review" in task.data) {
307
+ declareRef(task.data.review.repo, task.data.review.commit)
308
+ reviewSources.push({
309
+ repo: task.data.review.repo,
310
+ task: task.id,
311
+ ref:
312
+ task.data.review.source.kind === "pull-request"
313
+ ? task.data.review.source.fetchRef
314
+ : task.data.review.source.ref
315
+ .replace(/^refs\/remotes\/origin\//, "")
316
+ .replace(/^origin\//, ""),
317
+ })
318
+ } else if ("repo" in task.data) {
297
319
  declareRef(task.data.repo, task.data.base)
298
320
  for (const reference of task.data.repos ?? [])
299
321
  declareRef(reference.repo, reference.ref)
@@ -307,18 +329,17 @@ export class DoctorService extends Effect.Service<DoctorService>()(
307
329
  }
308
330
  }
309
331
 
310
- const repositoryList = gitAvailable
332
+ const repositoryList = versionControlAvailable
311
333
  ? yield* repositories.list(root)
312
334
  : []
313
- if (!gitAvailable) {
335
+ if (!versionControlAvailable) {
314
336
  add({
315
337
  id: "repository.inspection",
316
338
  category: "repository",
317
339
  level: "warning",
318
340
  status: "fail",
319
- message:
320
- "Repository, ref, remote, and worktree checks were skipped because Git is unavailable",
321
- remediation: "Install 'git' and rerun 'agency doctor'.",
341
+ message: `Repository, ref, remote, and workspace checks were skipped because the ${config.vcs ?? "git"} backend is unavailable`,
342
+ remediation: `Install '${config.vcs === "jj" ? "jj" : "git"}' and rerun 'agency doctor'.`,
322
343
  })
323
344
  }
324
345
  for (const repository of repositoryList) {
@@ -357,6 +378,36 @@ export class DoctorService extends Effect.Service<DoctorService>()(
357
378
  })
358
379
  if (!repositoryValid) continue
359
380
 
381
+ for (const source of reviewSources.filter(
382
+ (item) => item.repo === repository.alias,
383
+ )) {
384
+ const observed = yield* fs.runCommand(
385
+ [
386
+ "git",
387
+ "-C",
388
+ repository.path,
389
+ "ls-remote",
390
+ "origin",
391
+ source.ref,
392
+ ],
393
+ { captureOutput: true },
394
+ )
395
+ const available =
396
+ observed.exitCode === 0 && Boolean(observed.stdout.trim())
397
+ add({
398
+ id: `review.${source.task}.source`,
399
+ category: "repository",
400
+ level: "warning",
401
+ status: available ? "pass" : "fail",
402
+ message: available
403
+ ? `Review source '${source.ref}' is available for task '${source.task}'`
404
+ : `Review source '${source.ref}' is unavailable for task '${source.task}'; its pin remains usable`,
405
+ remediation: available
406
+ ? undefined
407
+ : "Use the pinned checkout or refresh after restoring the source.",
408
+ })
409
+ }
410
+
360
411
  for (const ref of [...(refs.get(repository.alias) ?? [])].sort()) {
361
412
  const local = yield* fs.runCommand(
362
413
  [
@@ -395,7 +446,7 @@ export class DoctorService extends Effect.Service<DoctorService>()(
395
446
  }
396
447
  }
397
448
 
398
- if (validation.valid && gitAvailable) {
449
+ if (validation.valid && versionControlAvailable) {
399
450
  const inspected = yield* Effect.either(worktrees.list(root))
400
451
  if (Either.isLeft(inspected)) {
401
452
  add({
@@ -360,6 +360,11 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
360
360
  "base",
361
361
  "pr",
362
362
  ].some((key) => updates[key as keyof TaskUpdates] !== undefined)
363
+ if ("review" in record.data && executionChange) {
364
+ return yield* new GraphMutationError({
365
+ message: `Review task '${id}' does not have writable execution metadata`,
366
+ })
367
+ }
363
368
  if ("phases" in record.data && executionChange) {
364
369
  return yield* new GraphMutationError({
365
370
  message: `Task '${id}' has multiple phases; update execution metadata on a phase instead`,
@@ -54,7 +54,10 @@ interface RepositoryRecord {
54
54
  readonly target: string | null
55
55
  }
56
56
 
57
- type ExecutionData = PhaseData | Extract<TaskData, { readonly repo: string }>
57
+ type ExecutionData =
58
+ | PhaseData
59
+ | Extract<TaskData, { readonly repo: string }>
60
+ | Extract<TaskData, { readonly review: unknown }>
58
61
 
59
62
  export interface GraphOptions {
60
63
  readonly cwd?: string
@@ -548,10 +551,18 @@ export class GraphService extends Effect.Service<GraphService>()(
548
551
  const executionRepositories = (
549
552
  data: TaskData | PhaseData,
550
553
  ): readonly string[] =>
551
- "repo" in data
552
- ? [data.repo, ...(data.repos ?? []).map((item) => item.repo)]
553
- : []
554
+ "review" in data
555
+ ? [data.review.repo]
556
+ : "repo" in data
557
+ ? [data.repo, ...(data.repos ?? []).map((item) => item.repo)]
558
+ : []
554
559
  const executionEdges = (id: string, data: TaskData | PhaseData) => {
560
+ if ("review" in data) {
561
+ edges.push(
562
+ edge("references", id, repositoryNodeId(data.review.repo)),
563
+ )
564
+ return
565
+ }
555
566
  if (!("repo" in data)) return
556
567
  edges.push(edge("writes", id, repositoryNodeId(data.repo)))
557
568
  for (const reference of data.repos ?? []) {
@@ -650,8 +661,9 @@ export class GraphService extends Effect.Service<GraphService>()(
650
661
  const executionDetails = (entityPath: string, data: ExecutionData) =>
651
662
  Effect.gen(function* () {
652
663
  const directory = entityPath.replace(/\/(?:TASK|PHASE)\.md$/, "")
653
- const checkoutPath = join(directory, "code", data.repo)
654
- const repositoryPath = join(root, "repos", data.repo)
664
+ const repo = "review" in data ? data.review.repo : data.repo
665
+ const checkoutPath = join(directory, "code", repo)
666
+ const repositoryPath = join(root, "repos", repo)
655
667
  const materialized = yield* fs.isDirectory(checkoutPath)
656
668
  const result: {
657
669
  workspace?: GraphExecutionWorkspace
@@ -666,58 +678,110 @@ export class GraphService extends Effect.Service<GraphService>()(
666
678
  }
667
679
  }
668
680
  if (include.has("git")) {
669
- result.git = {
670
- branch: data.branch,
671
- base: data.base,
672
- branchCommit: yield* run(fs, [
673
- "git",
674
- "-C",
675
- repositoryPath,
676
- "rev-parse",
677
- `${data.branch}^{commit}`,
678
- ]),
679
- baseCommit: yield* run(fs, [
680
- "git",
681
- "-C",
682
- repositoryPath,
683
- "rev-parse",
684
- `${data.base}^{commit}`,
685
- ]),
686
- checkoutCommit: materialized
687
- ? yield* run(fs, [
688
- "git",
689
- "-C",
690
- checkoutPath,
691
- "rev-parse",
692
- "HEAD",
693
- ])
694
- : null,
695
- checkoutBranch: materialized
696
- ? yield* run(fs, [
697
- "git",
698
- "-C",
699
- checkoutPath,
700
- "symbolic-ref",
701
- "--quiet",
702
- "--short",
703
- "HEAD",
704
- ])
705
- : null,
706
- dirty: materialized
707
- ? ((status) =>
708
- status === null ? null : status.length > 0)(
709
- yield* runText(fs, [
681
+ result.git =
682
+ "review" in data
683
+ ? {
684
+ pinnedCommit: data.review.commit,
685
+ sourceCommit: ((value) =>
686
+ value?.split(/\s+/)[0] ?? null)(
687
+ yield* run(fs, [
688
+ "git",
689
+ "-C",
690
+ repositoryPath,
691
+ "ls-remote",
692
+ "origin",
693
+ data.review.source.kind === "pull-request"
694
+ ? data.review.source.fetchRef
695
+ : data.review.source.ref
696
+ .replace(/^refs\/remotes\/origin\//, "")
697
+ .replace(/^origin\//, ""),
698
+ ]),
699
+ ),
700
+ checkoutCommit: materialized
701
+ ? yield* run(fs, [
702
+ "git",
703
+ "-C",
704
+ checkoutPath,
705
+ "rev-parse",
706
+ "HEAD",
707
+ ])
708
+ : null,
709
+ checkoutBranch: materialized
710
+ ? yield* run(fs, [
711
+ "git",
712
+ "-C",
713
+ checkoutPath,
714
+ "symbolic-ref",
715
+ "--quiet",
716
+ "--short",
717
+ "HEAD",
718
+ ])
719
+ : null,
720
+ dirty: materialized
721
+ ? ((status) =>
722
+ status === null ? null : status.length > 0)(
723
+ yield* runText(fs, [
724
+ "git",
725
+ "-C",
726
+ checkoutPath,
727
+ "status",
728
+ "--porcelain",
729
+ ]),
730
+ )
731
+ : null,
732
+ }
733
+ : {
734
+ branch: data.branch,
735
+ base: data.base,
736
+ branchCommit: yield* run(fs, [
710
737
  "git",
711
738
  "-C",
712
- checkoutPath,
713
- "status",
714
- "--porcelain",
739
+ repositoryPath,
740
+ "rev-parse",
741
+ `${data.branch}^{commit}`,
715
742
  ]),
716
- )
717
- : null,
718
- }
743
+ baseCommit: yield* run(fs, [
744
+ "git",
745
+ "-C",
746
+ repositoryPath,
747
+ "rev-parse",
748
+ `${data.base}^{commit}`,
749
+ ]),
750
+ checkoutCommit: materialized
751
+ ? yield* run(fs, [
752
+ "git",
753
+ "-C",
754
+ checkoutPath,
755
+ "rev-parse",
756
+ "HEAD",
757
+ ])
758
+ : null,
759
+ checkoutBranch: materialized
760
+ ? yield* run(fs, [
761
+ "git",
762
+ "-C",
763
+ checkoutPath,
764
+ "symbolic-ref",
765
+ "--quiet",
766
+ "--short",
767
+ "HEAD",
768
+ ])
769
+ : null,
770
+ dirty: materialized
771
+ ? ((status) =>
772
+ status === null ? null : status.length > 0)(
773
+ yield* runText(fs, [
774
+ "git",
775
+ "-C",
776
+ checkoutPath,
777
+ "status",
778
+ "--porcelain",
779
+ ]),
780
+ )
781
+ : null,
782
+ }
719
783
  }
720
- if (include.has("pr")) {
784
+ if (include.has("pr") && !("review" in data)) {
721
785
  if (!data.pr) {
722
786
  result.pr = { url: null, state: "none" }
723
787
  } else {
@@ -879,6 +943,7 @@ export class GraphService extends Effect.Service<GraphService>()(
879
943
  version: GRAPH_VERSION,
880
944
  workbase: {
881
945
  version: config.version,
946
+ vcs: config.vcs ?? "git",
882
947
  ...(include.has("workspace") ? { root } : {}),
883
948
  },
884
949
  filters,