@markjaquith/agency 2.61.0 → 2.61.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.61.0",
3
+ "version": "2.61.2",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
package/src/cli.test.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { afterAll, afterEach, describe, expect, test } from "bun:test"
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ describe,
5
+ expect,
6
+ setDefaultTimeout,
7
+ test,
8
+ } from "bun:test"
2
9
  import { access, chmod, mkdir, realpath, stat, symlink } from "node:fs/promises"
3
10
  import { join, sep } from "node:path"
4
11
  import errorFixture from "../fixtures/protocol/error.json"
@@ -9,6 +16,8 @@ const projectRoot = join(import.meta.dir, "..")
9
16
  const cliPath = join(projectRoot, "cli.ts")
10
17
  const isolatedConfigHome = await createTempDir()
11
18
 
19
+ setDefaultTimeout(15_000)
20
+
12
21
  afterAll(() => cleanupTempDir(isolatedConfigHome))
13
22
 
14
23
  interface CliResult {
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { Effect } from "effect"
3
- import { mkdir } from "node:fs/promises"
3
+ import { chmod, mkdir } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { TaskService } from "../services/TaskService"
6
6
  import {
@@ -99,6 +99,44 @@ describe("sync command", () => {
99
99
  })
100
100
  })
101
101
 
102
+ test("groups repeated human-readable warnings by affected target", async () => {
103
+ await runTestEffect(
104
+ TaskService.pipe(
105
+ Effect.flatMap((service) =>
106
+ service.create(
107
+ {
108
+ id: "second",
109
+ ticketUrl: null,
110
+ repo: "agency",
111
+ branch: "task/second",
112
+ base: "main",
113
+ },
114
+ root,
115
+ ),
116
+ ),
117
+ ),
118
+ )
119
+ const originalPath = process.env.PATH
120
+ const bin = join(root, "bin")
121
+ await mkdir(bin)
122
+ await Bun.write(
123
+ join(bin, "gh"),
124
+ '#!/bin/sh\necho "provider unavailable" >&2\nexit 1\n',
125
+ )
126
+ await chmod(join(bin, "gh"), 0o755)
127
+ process.env.PATH = `${bin}:${originalPath}`
128
+ try {
129
+ const logs = await captureLogs(() =>
130
+ runTestEffect(sync({ cwd: root, dryRun: true })),
131
+ )
132
+ expect(
133
+ logs.filter((line) => line.includes("provider unavailable")),
134
+ ).toEqual(["Warning 'task:example', 'task:second': provider unavailable"])
135
+ } finally {
136
+ process.env.PATH = originalPath
137
+ }
138
+ })
139
+
102
140
  test("reports human-readable progress without polluting JSON output", async () => {
103
141
  const updates: string[] = []
104
142
  const progress: Progress = {
@@ -8,6 +8,27 @@ interface SyncCommandOptions extends BaseCommandOptions {
8
8
  readonly dryRun?: boolean
9
9
  }
10
10
 
11
+ interface Notice {
12
+ readonly kind: string
13
+ readonly target: string
14
+ readonly message: string
15
+ readonly action?: string
16
+ }
17
+
18
+ const groupedNotices = <T extends Notice>(notices: readonly T[]) => {
19
+ const groups = new Map<string, { notice: T; targets: string[] }>()
20
+ for (const notice of notices) {
21
+ const key = JSON.stringify([notice.kind, notice.message, notice.action])
22
+ const group = groups.get(key)
23
+ if (group) group.targets.push(notice.target)
24
+ else groups.set(key, { notice, targets: [notice.target] })
25
+ }
26
+ return groups.values()
27
+ }
28
+
29
+ const formatTargets = (targets: readonly string[]) =>
30
+ targets.map((target) => `'${target}'`).join(", ")
31
+
11
32
  export const sync = (
12
33
  options: SyncCommandOptions = {},
13
34
  progress: Progress = createProgress({
@@ -62,9 +83,11 @@ export const sync = (
62
83
  `${change.status === "applied" ? "Applied" : "Planned"} ${change.kind} '${change.target}': ${change.message}`,
63
84
  )
64
85
  }
65
- for (const warning of result.warnings) {
86
+ for (const { notice: warning, targets } of groupedNotices(
87
+ result.warnings,
88
+ )) {
66
89
  log(
67
- `Warning '${warning.target}': ${warning.message}${warning.action ? `. ${warning.action}` : ""}`,
90
+ `Warning ${formatTargets(targets)}: ${warning.message}${warning.action ? `. ${warning.action}` : ""}`,
68
91
  )
69
92
  }
70
93
  for (const issue of result.repositories.unresolved) {
@@ -72,9 +95,11 @@ export const sync = (
72
95
  `Unresolved repository '${issue.alias}': ${issue.message}. ${issue.action}`,
73
96
  )
74
97
  }
75
- for (const issue of result.unresolved) {
98
+ for (const { notice: issue, targets } of groupedNotices(
99
+ result.unresolved,
100
+ )) {
76
101
  log(
77
- `Unresolved '${issue.target}': ${issue.message}${issue.action ? `. ${issue.action}` : ""}`,
102
+ `Unresolved ${formatTargets(targets)}: ${issue.message}${issue.action ? `. ${issue.action}` : ""}`,
78
103
  )
79
104
  }
80
105
  if (
@@ -239,7 +239,15 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
239
239
  })
240
240
  if (!url)
241
241
  throw new Error("GitHub CLI did not return a pull request URL")
242
- return { ...normalizePullRequestRecord(url), draft }
242
+ const normalized = normalizePullRequestRecord(url)
243
+ return {
244
+ ...normalized,
245
+ headRepository: repository,
246
+ headBranch: execution.branch,
247
+ baseRepository: normalized.repository,
248
+ baseBranch: execution.base,
249
+ draft,
250
+ }
243
251
  },
244
252
  catch: (cause) =>
245
253
  new PullRequestError({
@@ -249,7 +257,8 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
249
257
  if (
250
258
  config.delivery &&
251
259
  (record.provider !== config.delivery.provider ||
252
- record.repository.toLowerCase() !== repository.toLowerCase())
260
+ (record.headRepository ?? record.repository).toLowerCase() !==
261
+ repository.toLowerCase())
253
262
  ) {
254
263
  return yield* new PullRequestError({
255
264
  message:
@@ -64,14 +64,17 @@ case "$*" in
64
64
  *mergeable*) ;;
65
65
  *) echo "mergeable field was not requested" >&2; exit 2 ;;
66
66
  esac
67
+ case "$*" in
68
+ *baseRepository*) echo "unsupported baseRepository field was requested" >&2; exit 3 ;;
69
+ esac
67
70
  if [ "$2" = "view" ]; then
68
71
  cat <<'JSON'
69
- {"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}
72
+ {"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}
70
73
  JSON
71
74
  exit 0
72
75
  fi
73
76
  cat <<'JSON'
74
- [{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}]
77
+ [{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}]
75
78
  JSON
76
79
  `,
77
80
  )
@@ -172,6 +175,7 @@ pr: null
172
175
  )
173
176
  const invocation = await Bun.file(capture).text()
174
177
  expect(invocation).toContain("--repo\n")
178
+ expect(invocation).not.toContain("baseRepository")
175
179
  expect(invocation).toContain("GIT_DIR=")
176
180
  expect(invocation).toContain(".jj")
177
181
 
@@ -307,6 +311,10 @@ pr: null
307
311
  state: "merged",
308
312
  draft: false,
309
313
  merged: true,
314
+ headRepository: "example/agency",
315
+ headBranch: "feat/example",
316
+ baseRepository: "example/agency",
317
+ baseBranch: "main",
310
318
  mergeable: true,
311
319
  },
312
320
  claim: { state: "released", sessionId: "session-1" },
@@ -357,7 +365,7 @@ pr: null
357
365
  join(root, "bin", "gh"),
358
366
  `#!/bin/sh
359
367
  cat <<'JSON'
360
- {"number":42,"state":"OPEN","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","url":"https://github.com/example/agency/pull/42","mergedAt":null,"mergeCommit":null,"mergeable":"CONFLICTING"}
368
+ {"number":42,"state":"OPEN","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"baseRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":null,"mergeCommit":null,"mergeable":"CONFLICTING"}
361
369
  JSON
362
370
  `,
363
371
  )
@@ -747,6 +755,89 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
747
755
  expect(task.data).toMatchObject({ status: "open" })
748
756
  })
749
757
 
758
+ test("reconciles a merged upstream PR whose head is the writable fork", async () => {
759
+ await runTestEffect(
760
+ TaskService.pipe(
761
+ Effect.flatMap((service) =>
762
+ service.create(
763
+ {
764
+ id: "forked",
765
+ ticketUrl: null,
766
+ repo: "agency",
767
+ branch: "task/polish-readme",
768
+ base: "main",
769
+ },
770
+ root,
771
+ ),
772
+ ),
773
+ ),
774
+ )
775
+ await runTestEffect(
776
+ WorktreeService.pipe(
777
+ Effect.flatMap((service) =>
778
+ service.materialize("forked", undefined, root),
779
+ ),
780
+ ),
781
+ )
782
+ await git(
783
+ ["remote", "set-url", "origin", "git@github.com:markjaquith/Pasted.git"],
784
+ join(root, "repos/agency"),
785
+ )
786
+ await runTestEffect(
787
+ PullRequestService.pipe(
788
+ Effect.flatMap((service) =>
789
+ service.setUrl(
790
+ "forked",
791
+ undefined,
792
+ "https://github.com/getpasted/pasted/pull/17",
793
+ root,
794
+ ),
795
+ ),
796
+ ),
797
+ )
798
+ await Bun.write(
799
+ join(root, "bin", "gh"),
800
+ `#!/bin/sh
801
+ cat <<'JSON'
802
+ {"number":17,"state":"MERGED","title":"Polish README","isDraft":false,"headRefName":"task/polish-readme","baseRefName":"main","headRepository":{"nameWithOwner":"markjaquith/Pasted"},"baseRepository":{"nameWithOwner":"getpasted/pasted"},"url":"https://github.com/getpasted/pasted/pull/17","mergedAt":"2026-08-11T00:00:00Z","mergeCommit":{"oid":"241d45da1115ef685c91a5e6882d118c16801550"},"mergeable":"UNKNOWN"}
803
+ JSON
804
+ `,
805
+ )
806
+ await chmod(join(root, "bin", "gh"), 0o755)
807
+
808
+ const applied = await runTestEffect(
809
+ SyncService.pipe(
810
+ Effect.flatMap((service) =>
811
+ service.reconcile({ cwd: root, apply: true }),
812
+ ),
813
+ ),
814
+ )
815
+ expect(
816
+ applied.unresolved.filter((notice) => notice.target === "task:forked"),
817
+ ).toEqual([])
818
+ expect(applied.changes.map((change) => change.kind)).toEqual([
819
+ "record-pr",
820
+ "mark-done",
821
+ ])
822
+ const task = await runTestEffect(
823
+ TaskService.pipe(
824
+ Effect.flatMap((service) => service.show("forked", root)),
825
+ ),
826
+ )
827
+ expect(task.data).toMatchObject({
828
+ status: "done",
829
+ pr: {
830
+ repository: "getpasted/pasted",
831
+ headRepository: "markjaquith/Pasted",
832
+ headBranch: "task/polish-readme",
833
+ baseRepository: "getpasted/pasted",
834
+ baseBranch: "main",
835
+ state: "merged",
836
+ merged: true,
837
+ },
838
+ })
839
+ })
840
+
750
841
  test("leaves non-PR completion unchanged when a matching PR is discoverable", async () => {
751
842
  await runTestEffect(
752
843
  TaskService.pipe(
@@ -926,4 +1017,44 @@ exit 9
926
1017
  ),
927
1018
  ).toEqual([])
928
1019
  })
1020
+
1021
+ test("keeps pull request query failures concise", async () => {
1022
+ await runTestEffect(
1023
+ TaskService.pipe(
1024
+ Effect.flatMap((service) =>
1025
+ service.create(
1026
+ {
1027
+ id: "unavailable",
1028
+ ticketUrl: null,
1029
+ repo: "agency",
1030
+ branch: "feat/unavailable",
1031
+ base: "main",
1032
+ },
1033
+ root,
1034
+ ),
1035
+ ),
1036
+ ),
1037
+ )
1038
+ await Bun.write(
1039
+ join(root, "bin", "gh"),
1040
+ `#!/bin/sh
1041
+ echo "Unknown JSON field: unsupported" >&2
1042
+ echo "Available fields:" >&2
1043
+ echo " additions" >&2
1044
+ exit 1
1045
+ `,
1046
+ )
1047
+ await chmod(join(root, "bin", "gh"), 0o755)
1048
+
1049
+ const result = await runTestEffect(
1050
+ SyncService.pipe(
1051
+ Effect.flatMap((service) => service.reconcile({ cwd: root })),
1052
+ ),
1053
+ )
1054
+ expect(result.warnings).toContainEqual({
1055
+ kind: "pr-discovery-unavailable",
1056
+ target: "task:unavailable",
1057
+ message: "Unknown JSON field: unsupported",
1058
+ })
1059
+ })
929
1060
  })
@@ -153,6 +153,15 @@ const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
153
153
  const originRef = (ref: string) =>
154
154
  ref.replace(/^refs\/remotes\/origin\//, "").replace(/^origin\//, "")
155
155
 
156
+ const GITHUB_PR_FIELDS =
157
+ "number,state,title,isDraft,headRefName,baseRefName,headRepository,url,mergedAt,mergeCommit,mergeable"
158
+
159
+ const commandErrorSummary = (stderr: string, fallback: string) =>
160
+ stderr
161
+ .split("\n")
162
+ .map((line) => line.trim())
163
+ .find(Boolean) ?? fallback
164
+
156
165
  export class SyncService extends Effect.Service<SyncService>()("SyncService", {
157
166
  sync: () => ({
158
167
  reconcile: (
@@ -349,7 +358,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
349
358
  "view",
350
359
  existing.url,
351
360
  "--json",
352
- "number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
361
+ GITHUB_PR_FIELDS,
353
362
  ],
354
363
  {
355
364
  cwd: repositoryPath,
@@ -372,7 +381,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
372
381
  "--state",
373
382
  "all",
374
383
  "--json",
375
- "number,state,title,isDraft,headRefName,baseRefName,url,mergedAt,mergeCommit,mergeable",
384
+ GITHUB_PR_FIELDS,
376
385
  ],
377
386
  {
378
387
  cwd: repositoryPath,
@@ -773,19 +782,6 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
773
782
  const query = prQueries.get(record.key)!
774
783
  const { remoteUrl, remoteRepository } = query
775
784
 
776
- if (
777
- existing &&
778
- remoteRepository.toLowerCase() !== existing.repository.toLowerCase()
779
- ) {
780
- prConflict = true
781
- unresolved.push({
782
- kind: "pr-repository-conflict",
783
- target: record.key,
784
- message: `Recorded PR repository does not match writable repository remote '${remoteName}'`,
785
- action: "Correct the configured remote or recorded PR",
786
- })
787
- }
788
-
789
785
  if (config.delivery && !remoteUrl) {
790
786
  warnings.push({
791
787
  kind: "delivery-remote-unavailable",
@@ -812,8 +808,9 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
812
808
  } else if (
813
809
  parsed.right &&
814
810
  (parsed.right.provider !== config.delivery.provider ||
815
- parsed.right.repository.toLowerCase() !==
816
- remoteRepository.toLowerCase())
811
+ (
812
+ parsed.right.headRepository ?? parsed.right.repository
813
+ ).toLowerCase() !== remoteRepository.toLowerCase())
817
814
  ) {
818
815
  if (parsed.right) {
819
816
  prConflict = true
@@ -836,8 +833,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
836
833
  warnings.push({
837
834
  kind: existing ? "pr-unavailable" : "pr-discovery-unavailable",
838
835
  target: record.key,
839
- message:
840
- queried.stderr.trim() || "Could not query delivery provider",
836
+ message: commandErrorSummary(
837
+ queried.stderr,
838
+ "Could not query delivery provider",
839
+ ),
841
840
  })
842
841
  }
843
842
  } else if (existing) {
@@ -850,14 +849,18 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
850
849
  current = recordFromGitHubJson(detail)
851
850
  pr = { ...detail, ...current }
852
851
  if (
853
- detail.headRefName !== data.branch ||
854
- detail.baseRefName !== data.base
852
+ current.headRepository?.toLowerCase() !==
853
+ remoteRepository.toLowerCase() ||
854
+ current.headBranch !== data.branch ||
855
+ current.baseRepository?.toLowerCase() !==
856
+ current.repository.toLowerCase() ||
857
+ current.baseBranch !== data.base
855
858
  ) {
856
859
  prConflict = true
857
860
  unresolved.push({
858
- kind: "pr-branch-conflict",
861
+ kind: "pr-repository-conflict",
859
862
  target: record.key,
860
- message: `Recorded PR branches do not match '${data.branch}' -> '${data.base}'`,
863
+ message: `Recorded PR head does not match '${remoteRepository}:${data.branch}' or base '${current.repository}:${data.base}'`,
861
864
  action: "Correct the declaration or recorded PR URL",
862
865
  })
863
866
  }
@@ -866,7 +869,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
866
869
  warnings.push({
867
870
  kind: "pr-unavailable",
868
871
  target: record.key,
869
- message: `Could not inspect ${existing.url}: ${viewed.stderr.trim()}`,
872
+ message: `Could not inspect ${existing.url}: ${commandErrorSummary(viewed.stderr, "GitHub query failed")}`,
870
873
  })
871
874
  }
872
875
  } else {
@@ -883,6 +886,20 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
883
886
  if (matches.length === 1) {
884
887
  current = recordFromGitHubJson(matches[0]!)
885
888
  pr = { ...matches[0], ...current }
889
+ if (
890
+ current.headRepository?.toLowerCase() !==
891
+ remoteRepository.toLowerCase() ||
892
+ current.baseRepository?.toLowerCase() !==
893
+ current.repository.toLowerCase()
894
+ ) {
895
+ prConflict = true
896
+ unresolved.push({
897
+ kind: "pr-repository-conflict",
898
+ target: record.key,
899
+ message: `Discovered PR repositories do not match writable repository '${remoteRepository}' and base '${current.repository}'`,
900
+ action: "Record the authoritative PR URL manually",
901
+ })
902
+ }
886
903
  } else if (matches.length > 1) {
887
904
  unresolved.push({
888
905
  kind: "multiple-prs",
@@ -895,8 +912,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
895
912
  warnings.push({
896
913
  kind: "pr-discovery-unavailable",
897
914
  target: record.key,
898
- message:
899
- listed.stderr.trim() || "Could not discover pull requests",
915
+ message: commandErrorSummary(
916
+ listed.stderr,
917
+ "Could not discover pull requests",
918
+ ),
900
919
  })
901
920
  }
902
921
  }
@@ -104,8 +104,24 @@ describe("delivery commands", () => {
104
104
  isDraft: false,
105
105
  }
106
106
  expect(
107
- recordFromGitHubJson({ ...base, state: "OPEN", mergeable: "MERGEABLE" }),
108
- ).toMatchObject({ state: "open", merged: false, mergeable: true })
107
+ recordFromGitHubJson({
108
+ ...base,
109
+ state: "OPEN",
110
+ headRefName: "feat/example",
111
+ baseRefName: "main",
112
+ headRepository: { nameWithOwner: "fork/agency" },
113
+ baseRepository: { nameWithOwner: "example/agency" },
114
+ mergeable: "MERGEABLE",
115
+ }),
116
+ ).toMatchObject({
117
+ state: "open",
118
+ merged: false,
119
+ headRepository: "fork/agency",
120
+ headBranch: "feat/example",
121
+ baseRepository: "example/agency",
122
+ baseBranch: "main",
123
+ mergeable: true,
124
+ })
109
125
  expect(
110
126
  recordFromGitHubJson({
111
127
  ...base,
@@ -156,11 +156,24 @@ export const recordFromGitHubUrl = (url: string): PullRequestRecord => {
156
156
  export const recordFromGitHubJson = (value: Record<string, unknown>) => {
157
157
  const url = typeof value.url === "string" ? value.url : ""
158
158
  const record = recordFromGitHubUrl(url)
159
+ const repositoryName = (repository: unknown) => {
160
+ if (!repository || typeof repository !== "object") return undefined
161
+ const nameWithOwner = (repository as Record<string, unknown>).nameWithOwner
162
+ return typeof nameWithOwner === "string" && nameWithOwner
163
+ ? nameWithOwner
164
+ : undefined
165
+ }
159
166
  const githubState = String(value.state ?? "OPEN").toLowerCase()
160
167
  const merged = githubState === "merged" || value.mergedAt != null
161
168
  const mergeable = String(value.mergeable ?? "UNKNOWN").toLowerCase()
162
169
  return {
163
170
  ...record,
171
+ headRepository: repositoryName(value.headRepository),
172
+ headBranch:
173
+ typeof value.headRefName === "string" ? value.headRefName : undefined,
174
+ baseRepository: repositoryName(value.baseRepository) ?? record.repository,
175
+ baseBranch:
176
+ typeof value.baseRefName === "string" ? value.baseRefName : undefined,
164
177
  state: merged ? "merged" : githubState === "closed" ? "closed" : "open",
165
178
  draft: value.isDraft === true,
166
179
  merged,
@@ -72,6 +72,10 @@ const GitHubPullRequestUrl = NonEmptyString.pipe(
72
72
  export const PullRequestRecord = Schema.Struct({
73
73
  provider: EntityId,
74
74
  repository: NonEmptyString,
75
+ headRepository: Schema.optional(NonEmptyString),
76
+ headBranch: Schema.optional(NonEmptyString),
77
+ baseRepository: Schema.optional(NonEmptyString),
78
+ baseBranch: Schema.optional(NonEmptyString),
75
79
  identifier: NonEmptyString,
76
80
  url: Url,
77
81
  state: Schema.Literal("open", "closed", "merged"),