@markjaquith/agency 2.69.0 → 2.69.1

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.69.0",
3
+ "version": "2.69.1",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -575,6 +575,8 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
575
575
  })
576
576
 
577
577
  test("materializes missing workspaces but leaves branch conflicts unresolved", async () => {
578
+ await Bun.write(join(root, "bin", "gh"), "#!/bin/sh\nprintf '[]\\n'\n")
579
+ await chmod(join(root, "bin", "gh"), 0o755)
578
580
  for (const [id, branch] of [
579
581
  ["missing", "feat/missing"],
580
582
  ["conflict", "feat/conflict"],
@@ -649,6 +651,158 @@ process.stdout.write(${JSON.stringify(JSON.stringify(record))})
649
651
  ).toBe(false)
650
652
  })
651
653
 
654
+ test("reconciles a recorded merged PR without materializing an absent checkout", async () => {
655
+ await runTestEffect(
656
+ TaskService.pipe(
657
+ Effect.flatMap((service) =>
658
+ service.create(
659
+ {
660
+ id: "recorded-merged",
661
+ ticketUrl: null,
662
+ repo: "agency",
663
+ branch: "feat/example",
664
+ base: "main",
665
+ },
666
+ root,
667
+ ),
668
+ ),
669
+ ),
670
+ )
671
+ await git(
672
+ ["remote", "set-url", "origin", "git@github.com:example/agency.git"],
673
+ join(root, "repos/agency"),
674
+ )
675
+ await runTestEffect(
676
+ PullRequestService.pipe(
677
+ Effect.flatMap((service) =>
678
+ service.setUrl(
679
+ "recorded-merged",
680
+ undefined,
681
+ "https://github.com/example/agency/pull/42",
682
+ root,
683
+ ),
684
+ ),
685
+ ),
686
+ )
687
+
688
+ const applied = await runTestEffect(
689
+ SyncService.pipe(
690
+ Effect.flatMap((service) =>
691
+ service.reconcile({
692
+ cwd: root,
693
+ apply: true,
694
+ taskId: "recorded-merged",
695
+ }),
696
+ ),
697
+ ),
698
+ )
699
+ expect(applied.changes.map((change) => change.kind)).toEqual([
700
+ "record-pr",
701
+ "mark-done",
702
+ ])
703
+ expect(applied.executions[0]?.checkouts).toEqual([])
704
+ expect(
705
+ await Bun.file(join(root, "tasks/recorded-merged/code/agency")).exists(),
706
+ ).toBe(false)
707
+ })
708
+
709
+ test("reconciles a uniquely discovered merged PR without materializing", async () => {
710
+ await runTestEffect(
711
+ TaskService.pipe(
712
+ Effect.flatMap((service) =>
713
+ service.create(
714
+ {
715
+ id: "discovered-merged",
716
+ ticketUrl: null,
717
+ repo: "agency",
718
+ branch: "feat/example",
719
+ base: "main",
720
+ },
721
+ root,
722
+ ),
723
+ ),
724
+ ),
725
+ )
726
+ await git(
727
+ ["remote", "set-url", "origin", "git@github.com:example/agency.git"],
728
+ join(root, "repos/agency"),
729
+ )
730
+
731
+ const applied = await runTestEffect(
732
+ SyncService.pipe(
733
+ Effect.flatMap((service) =>
734
+ service.reconcile({
735
+ cwd: root,
736
+ apply: true,
737
+ taskId: "discovered-merged",
738
+ }),
739
+ ),
740
+ ),
741
+ )
742
+ expect(applied.changes.map((change) => change.kind)).toEqual([
743
+ "record-pr",
744
+ "mark-done",
745
+ ])
746
+ expect(applied.executions[0]?.checkouts).toEqual([])
747
+ expect(
748
+ await Bun.file(
749
+ join(root, "tasks/discovered-merged/code/agency"),
750
+ ).exists(),
751
+ ).toBe(false)
752
+ })
753
+
754
+ test("materializes when discovered PR evidence is ambiguous", async () => {
755
+ await runTestEffect(
756
+ TaskService.pipe(
757
+ Effect.flatMap((service) =>
758
+ service.create(
759
+ {
760
+ id: "ambiguous",
761
+ ticketUrl: null,
762
+ repo: "agency",
763
+ branch: "feat/example",
764
+ base: "main",
765
+ },
766
+ root,
767
+ ),
768
+ ),
769
+ ),
770
+ )
771
+ const gh = await Bun.file(join(root, "bin", "gh")).text()
772
+ await Bun.write(
773
+ join(root, "bin", "gh"),
774
+ gh
775
+ .replace('[{"number":42', '[{"number":42')
776
+ .replace(
777
+ "]\nJSON\n",
778
+ `,{\"number\":43,\"state\":\"MERGED\",\"title\":\"Ship again\",\"isDraft\":false,\"headRefName\":\"feat/example\",\"baseRefName\":\"main\",\"headRepository\":{\"nameWithOwner\":\"example/agency\"},\"url\":\"https://github.com/example/agency/pull/43\",\"mergedAt\":\"2100-01-01T00:00:00Z\",\"mergeCommit\":{\"oid\":\"def\"},\"mergeable\":\"MERGEABLE\"}]\nJSON\n`,
779
+ ),
780
+ )
781
+
782
+ const applied = await runTestEffect(
783
+ SyncService.pipe(
784
+ Effect.flatMap((service) =>
785
+ service.reconcile({
786
+ cwd: root,
787
+ apply: true,
788
+ taskId: "ambiguous",
789
+ }),
790
+ ),
791
+ ),
792
+ )
793
+ expect(applied.unresolved).toContainEqual(
794
+ expect.objectContaining({ kind: "multiple-prs" }),
795
+ )
796
+ expect(applied.changes).toContainEqual(
797
+ expect.objectContaining({ kind: "materialize-workspace" }),
798
+ )
799
+ expect(
800
+ await Bun.file(
801
+ join(root, "tasks/ambiguous/code/agency/README.md"),
802
+ ).text(),
803
+ ).toBe("example\n")
804
+ })
805
+
652
806
  test("leaves a missing checkout registration unresolved", async () => {
653
807
  await runTestEffect(
654
808
  TaskService.pipe(
@@ -162,6 +162,46 @@ const commandErrorSummary = (stderr: string, fallback: string) =>
162
162
  .map((line) => line.trim())
163
163
  .find(Boolean) ?? fallback
164
164
 
165
+ interface PullRequestQuery {
166
+ readonly remoteUrl: string | null
167
+ readonly remoteRepository: string
168
+ readonly result:
169
+ | {
170
+ readonly exitCode: number
171
+ readonly stdout: string
172
+ readonly stderr: string
173
+ }
174
+ | undefined
175
+ }
176
+
177
+ const mergedPullRequestFromGitHub = (
178
+ data: ExecutionData,
179
+ query: PullRequestQuery | undefined,
180
+ ) => {
181
+ if (!query?.result || query.result.exitCode !== 0) return null
182
+ const existing = data.pr ? normalizePullRequestRecord(data.pr) : null
183
+ const details = existing
184
+ ? [parseJson<Record<string, unknown>>(query.result.stdout, {})]
185
+ : parseJson<Record<string, unknown>[]>(query.result.stdout, []).filter(
186
+ (item) =>
187
+ item.headRefName === data.branch && item.baseRefName === data.base,
188
+ )
189
+ if (details.length !== 1) return null
190
+ const current = recordFromGitHubJson(details[0]!)
191
+ if (
192
+ current.merged !== true ||
193
+ current.headRepository?.toLowerCase() !==
194
+ query.remoteRepository.toLowerCase() ||
195
+ current.headBranch !== data.branch ||
196
+ current.baseRepository?.toLowerCase() !==
197
+ current.repository.toLowerCase() ||
198
+ current.baseBranch !== data.base
199
+ ) {
200
+ return null
201
+ }
202
+ return current
203
+ }
204
+
165
205
  export class SyncService extends Effect.Service<SyncService>()("SyncService", {
166
206
  sync: () => ({
167
207
  reconcile: (
@@ -463,6 +503,14 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
463
503
  let revision = record.revision
464
504
  const codePath = join(dirname(record.path), "code")
465
505
  const checkoutStates: CheckoutState[] = []
506
+ const query = prQueries.get(record.key)
507
+ const remoteMergedPr = config.delivery
508
+ ? null
509
+ : mergedPullRequestFromGitHub(data, query)
510
+ const skipCheckoutReconciliation =
511
+ remoteMergedPr !== null &&
512
+ data.claim?.state !== "active" &&
513
+ !data.completion
466
514
  let materialize = false
467
515
  let workspaceConflict = false
468
516
  const declared: readonly (
@@ -473,7 +521,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
473
521
  ...(data.repos ?? []),
474
522
  ]
475
523
 
476
- for (const checkout of declared) {
524
+ for (const checkout of skipCheckoutReconciliation ? [] : declared) {
477
525
  const repositoryPath = join(root, "repos", checkout.repo)
478
526
  const checkoutPath = join(codePath, checkout.repo)
479
527
  const kind = "branch" in checkout ? "writable" : "reference"
@@ -811,7 +859,11 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
811
859
  state: "none",
812
860
  }
813
861
  let prConflict = false
814
- const query = prQueries.get(record.key)!
862
+ if (!query) {
863
+ return yield* new SyncError({
864
+ message: `Missing pull request query for '${record.key}'`,
865
+ })
866
+ }
815
867
  const { remoteUrl, remoteRepository } = query
816
868
 
817
869
  if (config.delivery && !remoteUrl) {