@markjaquith/agency 3.2.6 → 3.2.8

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": "3.2.6",
3
+ "version": "3.2.8",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -0,0 +1,58 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { writeFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { cleanupTempDir, createTempDir } from "./test-utils"
5
+
6
+ const checkCommits = join(import.meta.dir, "../scripts/check-commits")
7
+
8
+ describe("check-commits", () => {
9
+ let root: string
10
+
11
+ function git(...args: string[]): void {
12
+ const result = Bun.spawnSync(["git", ...args], { cwd: root })
13
+ if (result.exitCode !== 0) {
14
+ throw new Error(new TextDecoder().decode(result.stderr))
15
+ }
16
+ }
17
+
18
+ function commit(subject: string, file: string): void {
19
+ writeFileSync(join(root, file), `${subject}\n`)
20
+ git("add", file)
21
+ git("commit", "-m", subject)
22
+ }
23
+
24
+ beforeEach(async () => {
25
+ root = await createTempDir()
26
+ git("init", "--initial-branch=main")
27
+ git("config", "user.email", "agency@example.com")
28
+ git("config", "user.name", "Agency Test")
29
+ commit("chore: initial commit", "initial.txt")
30
+ git("update-ref", "refs/remotes/origin/main", "HEAD")
31
+ })
32
+
33
+ afterEach(async () => cleanupTempDir(root))
34
+
35
+ test("allows merge commits with non-conventional subjects", () => {
36
+ git("checkout", "-b", "feature")
37
+ commit("feat: add feature", "feature.txt")
38
+ git("checkout", "-b", "topic", "main")
39
+ commit("fix: add topic", "topic.txt")
40
+ git("checkout", "feature")
41
+ git("merge", "--no-ff", "topic", "-m", "Combine topic into feature")
42
+
43
+ const result = Bun.spawnSync([checkCommits], { cwd: root })
44
+
45
+ expect(result.exitCode).toBe(0)
46
+ })
47
+
48
+ test("rejects non-conventional non-merge commits", () => {
49
+ commit("not a conventional commit", "invalid.txt")
50
+
51
+ const result = Bun.spawnSync([checkCommits], { cwd: root })
52
+
53
+ expect(result.exitCode).toBe(1)
54
+ expect(new TextDecoder().decode(result.stderr)).toContain(
55
+ "not a conventional commit",
56
+ )
57
+ })
58
+ })
@@ -4,6 +4,7 @@ import { chmod, mkdir, rm } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
6
  import { PullRequestService } from "./PullRequestService"
7
+ import { ReviewService } from "./ReviewService"
7
8
  import { SyncService } from "./SyncService"
8
9
  import { TaskService } from "./TaskService"
9
10
  import { WorktreeService } from "./WorktreeService"
@@ -1123,6 +1124,134 @@ exit 9
1123
1124
  ).toEqual([])
1124
1125
  })
1125
1126
 
1127
+ test("inspects workspace dirtiness concurrently", async () => {
1128
+ for (const id of ["first", "second", "third"]) {
1129
+ await runTestEffect(
1130
+ TaskService.pipe(
1131
+ Effect.flatMap((service) =>
1132
+ service.create(
1133
+ {
1134
+ id,
1135
+ ticketUrl: null,
1136
+ repo: "agency",
1137
+ branch: `feat/${id}`,
1138
+ base: "main",
1139
+ },
1140
+ root,
1141
+ ),
1142
+ ),
1143
+ ),
1144
+ )
1145
+ await runTestEffect(
1146
+ WorktreeService.pipe(
1147
+ Effect.flatMap((service) => service.materialize(id, undefined, root)),
1148
+ ),
1149
+ )
1150
+ }
1151
+
1152
+ const barrier = join(root, "status-barrier")
1153
+ await mkdir(barrier)
1154
+ const realGit = Bun.which("git")!
1155
+ await Bun.write(
1156
+ join(root, "bin", "git"),
1157
+ `#!/bin/sh
1158
+ case "$*" in
1159
+ *" status --porcelain"*)
1160
+ workspace=""
1161
+ previous=""
1162
+ for argument in "$@"; do
1163
+ if [ "$previous" = "-C" ]; then workspace="$argument"; fi
1164
+ previous="$argument"
1165
+ done
1166
+ id="$(basename "$(dirname "$(dirname "$workspace")")")"
1167
+ touch ${JSON.stringify(barrier)}/"$id"
1168
+ attempt=0
1169
+ while [ "$attempt" -lt 200 ]; do
1170
+ set -- ${JSON.stringify(barrier)}/*
1171
+ if [ -e "$1" ] && [ "$#" -ge 3 ]; then exec ${JSON.stringify(realGit)} -C "$workspace" status --porcelain; fi
1172
+ attempt=$((attempt + 1))
1173
+ sleep 0.01
1174
+ done
1175
+ echo "workspace inspections were serialized" >&2
1176
+ exit 9
1177
+ ;;
1178
+ esac
1179
+ exec ${JSON.stringify(realGit)} "$@"
1180
+ `,
1181
+ )
1182
+ await chmod(join(root, "bin", "git"), 0o755)
1183
+
1184
+ const result = await runTestEffect(
1185
+ SyncService.pipe(
1186
+ Effect.flatMap((service) => service.reconcile({ cwd: root })),
1187
+ ),
1188
+ )
1189
+ expect(
1190
+ result.warnings.filter(
1191
+ (warning) => warning.kind === "status-inspection-failed",
1192
+ ),
1193
+ ).toEqual([])
1194
+ })
1195
+
1196
+ test("queries review sources concurrently", async () => {
1197
+ const source = join(root, "source")
1198
+ for (const id of ["first", "second", "third"]) {
1199
+ const ref = `review-${id}`
1200
+ await git(["branch", ref, "main"], source)
1201
+ const review = await runTestEffect(
1202
+ ReviewService.pipe(
1203
+ Effect.flatMap((service) => service.resolve("agency", { ref }, root)),
1204
+ ),
1205
+ )
1206
+ await runTestEffect(
1207
+ TaskService.pipe(
1208
+ Effect.flatMap((service) =>
1209
+ service.create({ id, ticketUrl: null, review }, root),
1210
+ ),
1211
+ ),
1212
+ )
1213
+ }
1214
+
1215
+ const barrier = join(root, "review-query-barrier")
1216
+ await mkdir(barrier)
1217
+ const realGit = Bun.which("git")!
1218
+ await Bun.write(
1219
+ join(root, "bin", "git"),
1220
+ `#!/bin/sh
1221
+ case "$*" in
1222
+ *"ls-remote"*)
1223
+ ref=""
1224
+ for argument in "$@"; do ref="$argument"; done
1225
+ id="\${ref##*-}"
1226
+ touch ${JSON.stringify(barrier)}/"$id"
1227
+ attempt=0
1228
+ while [ "$attempt" -lt 200 ]; do
1229
+ set -- ${JSON.stringify(barrier)}/*
1230
+ if [ -e "$1" ] && [ "$#" -ge 3 ]; then printf '%s\t%s\n' '0123456789012345678901234567890123456789' "$ref"; exit 0; fi
1231
+ attempt=$((attempt + 1))
1232
+ sleep 0.01
1233
+ done
1234
+ echo "review source queries were serialized" >&2
1235
+ exit 9
1236
+ ;;
1237
+ esac
1238
+ exec ${JSON.stringify(realGit)} "$@"
1239
+ `,
1240
+ )
1241
+ await chmod(join(root, "bin", "git"), 0o755)
1242
+
1243
+ const result = await runTestEffect(
1244
+ SyncService.pipe(
1245
+ Effect.flatMap((service) => service.reconcile({ cwd: root })),
1246
+ ),
1247
+ )
1248
+ expect(
1249
+ result.warnings.filter(
1250
+ (warning) => warning.kind === "review-source-unavailable",
1251
+ ),
1252
+ ).toEqual([])
1253
+ })
1254
+
1126
1255
  test("keeps pull request query failures concise", async () => {
1127
1256
  await runTestEffect(
1128
1257
  TaskService.pipe(
@@ -493,6 +493,34 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
493
493
  { concurrency: 8 },
494
494
  ),
495
495
  )
496
+ const reviewSourceQueries = new Map(
497
+ yield* Effect.forEach(
498
+ reviewRecords,
499
+ (task) =>
500
+ Effect.gen(function* () {
501
+ if (!("review" in task.data)) return [task.id, null] as const
502
+ const repositoryPath = join(
503
+ root,
504
+ "repos",
505
+ task.data.review.repo,
506
+ )
507
+ const remote = yield* backend.remoteUrl(
508
+ repositoryPath,
509
+ "origin",
510
+ )
511
+ const source = yield* runExternal([
512
+ "git",
513
+ "ls-remote",
514
+ remote ?? "origin",
515
+ task.data.review.source.kind === "pull-request"
516
+ ? task.data.review.source.fetchRef
517
+ : originRef(task.data.review.source.ref),
518
+ ])
519
+ return [task.id, source] as const
520
+ }),
521
+ { concurrency: 8 },
522
+ ),
523
+ )
496
524
  const executionTotal = records.length + reviewRecords.length
497
525
  let reconciledExecutions = 0
498
526
  const reportExecution = (target: string) => {
@@ -504,6 +532,55 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
504
532
  target,
505
533
  })
506
534
  }
535
+ const checkoutRecords = records.filter((record) => {
536
+ if (record.data.completion) return false
537
+ const merged = config.delivery
538
+ ? null
539
+ : mergedPullRequestFromGitHub(
540
+ record.data,
541
+ prQueries.get(record.key),
542
+ )
543
+ return merged === null
544
+ })
545
+ const checkoutCandidates = checkoutRecords.flatMap((record) => {
546
+ const codePath = join(dirname(record.path), "code")
547
+ return [
548
+ { repo: record.data.repo, path: join(codePath, record.data.repo) },
549
+ ...(record.data.repos ?? []).map((reference) => ({
550
+ repo: reference.repo,
551
+ path: join(codePath, reference.repo),
552
+ })),
553
+ ]
554
+ })
555
+ const checkoutRepositoryPaths = [
556
+ ...new Set(
557
+ checkoutCandidates.map(({ repo }) => join(root, "repos", repo)),
558
+ ),
559
+ ]
560
+ yield* Effect.forEach(checkoutRepositoryPaths, listRegistered, {
561
+ concurrency: 8,
562
+ })
563
+ const dirtyByCheckoutPath = new Map<string, boolean | null>()
564
+ yield* Effect.forEach(
565
+ checkoutCandidates,
566
+ ({ repo, path }) =>
567
+ Effect.gen(function* () {
568
+ if (!(yield* fs.isDirectory(path))) return
569
+ const repositoryPath = join(root, "repos", repo)
570
+ const registered = registeredByRepository.get(repositoryPath)
571
+ if (!registered) return
572
+ const expectedPath = yield* fs.realPath(path)
573
+ const atPath = registered.find(
574
+ (item) => item.path === expectedPath,
575
+ )
576
+ if (!atPath) return
577
+ dirtyByCheckoutPath.set(
578
+ path,
579
+ atPath.dirty ?? (yield* backend.workspaceDirty(path)),
580
+ )
581
+ }),
582
+ { concurrency: 8 },
583
+ )
507
584
 
508
585
  for (const record of records.sort((a, b) =>
509
586
  a.key.localeCompare(b.key),
@@ -666,8 +743,11 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
666
743
 
667
744
  const dirty =
668
745
  exists && atPath
669
- ? (atPath.dirty ??
670
- (yield* backend.workspaceDirty(checkoutPath)))
746
+ ? atPath.dirty !== undefined
747
+ ? atPath.dirty
748
+ : dirtyByCheckoutPath.has(checkoutPath)
749
+ ? dirtyByCheckoutPath.get(checkoutPath)!
750
+ : yield* backend.workspaceDirty(checkoutPath)
671
751
  : null
672
752
  if (exists && atPath && dirty === null) {
673
753
  warnings.push({
@@ -1021,20 +1101,8 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
1021
1101
  status: apply ? "applied" : "planned",
1022
1102
  })
1023
1103
  }
1024
- const repositoryPath = join(root, "repos", data.review.repo)
1025
- const reviewRemote = yield* backend.remoteUrl(
1026
- repositoryPath,
1027
- "origin",
1028
- )
1029
- const source = yield* runExternal([
1030
- "git",
1031
- "ls-remote",
1032
- reviewRemote ?? "origin",
1033
- data.review.source.kind === "pull-request"
1034
- ? data.review.source.fetchRef
1035
- : originRef(data.review.source.ref),
1036
- ])
1037
- const sourceCommit = source.stdout.trim().split(/\s+/)[0] || null
1104
+ const source = reviewSourceQueries.get(task.id)
1105
+ const sourceCommit = source?.stdout.trim().split(/\s+/)[0] || null
1038
1106
  if (!sourceCommit) {
1039
1107
  warnings.push({
1040
1108
  kind: "review-source-unavailable",