@markjaquith/agency 2.71.8 → 2.71.10

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.71.8",
3
+ "version": "2.71.10",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -64,11 +64,13 @@
64
64
  "scripts": {
65
65
  "postinstall": "bun scripts/install-pi-extension.ts install",
66
66
  "preuninstall": "bun scripts/install-pi-extension.ts uninstall",
67
+ "benchmark:pr": "bun scripts/benchmark-pr.ts",
67
68
  "benchmark:status": "bun scripts/benchmark-status.ts",
68
69
  "benchmark:workbase": "bun scripts/benchmark-workbase.ts",
69
70
  "benchmark:doctor": "bun scripts/benchmark-doctor.ts",
70
71
  "benchmark:context": "bun scripts/benchmark-context.ts",
71
72
  "benchmark:finish": "bun scripts/benchmark-finish.ts",
73
+ "benchmark:push": "bun scripts/benchmark-push.ts",
72
74
  "benchmark:sync": "bun scripts/benchmark-sync.ts",
73
75
  "benchmark:task": "bun scripts/benchmark-task.ts",
74
76
  "benchmark:worktree": "bun scripts/benchmark-worktree.ts",
@@ -5,6 +5,7 @@ import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
6
  import { TaskService } from "./TaskService"
7
7
  import { PushService } from "./PushService"
8
+ import { parseGitCommits } from "./push-validation"
8
9
  import { WorktreeService } from "./WorktreeService"
9
10
 
10
11
  interface CommandResult {
@@ -39,6 +40,24 @@ const requireCommand = async (args: readonly string[], cwd?: string) => {
39
40
  }
40
41
 
41
42
  describe("PushService", () => {
43
+ test("parses and validates batched Git commit metadata", () => {
44
+ const output = [
45
+ "abc123\0Agency Test\0agency@example.com\0First change\0\x1e",
46
+ "def456\0Agency Test\0agency@example.com\0Second change\0\x1e",
47
+ ].join("\n")
48
+ const commits = parseGitCommits(output)
49
+
50
+ expect(
51
+ commits.map(({ commitId, description }) => ({ commitId, description })),
52
+ ).toEqual([
53
+ { commitId: "abc123", description: "First change" },
54
+ { commitId: "def456", description: "Second change" },
55
+ ])
56
+ expect(commits.every((commit) => commit.authorEmail.includes("@"))).toBe(
57
+ true,
58
+ )
59
+ })
60
+
42
61
  const roots: string[] = []
43
62
 
44
63
  afterEach(async () => {
@@ -4,6 +4,7 @@ import { FileSystemService } from "./FileSystemService"
4
4
  import { PhaseService } from "./PhaseService"
5
5
  import { TaskService } from "./TaskService"
6
6
  import { WorkbaseService } from "./WorkbaseService"
7
+ import { parseGitCommits, type PushCommitMetadata } from "./push-validation"
7
8
 
8
9
  class PushError extends Data.TaggedError("PushError")<{
9
10
  readonly message: string
@@ -15,16 +16,7 @@ interface CommandResult {
15
16
  readonly stderr: string
16
17
  }
17
18
 
18
- interface CommitMetadata {
19
- readonly commitId: string
20
- readonly changeId?: string
21
- readonly description: string
22
- readonly empty: boolean
23
- readonly authorName: string
24
- readonly authorEmail: string
25
- readonly conflict: boolean
26
- readonly parents: readonly string[]
27
- }
19
+ type CommitMetadata = PushCommitMetadata
28
20
 
29
21
  interface PushResult {
30
22
  readonly vcs: "git" | "jj"
@@ -105,29 +97,6 @@ const gitAncestor = (
105
97
  }),
106
98
  )
107
99
 
108
- const parseGitCommits = (output: string): readonly CommitMetadata[] =>
109
- output
110
- .split("\x1e")
111
- .map((record) => record.replace(/^\n+|\n+$/g, ""))
112
- .filter(Boolean)
113
- .map((record) => {
114
- const [
115
- commitId = "",
116
- authorName = "",
117
- authorEmail = "",
118
- description = "",
119
- ] = record.split("\0")
120
- return {
121
- commitId,
122
- description,
123
- empty: false,
124
- authorName,
125
- authorEmail,
126
- conflict: false,
127
- parents: [],
128
- }
129
- })
130
-
131
100
  const validateGitCommits = (
132
101
  commits: readonly CommitMetadata[],
133
102
  base: string,
@@ -194,11 +163,12 @@ const publishGit = (
194
163
  ["fetch", remote, `+refs/heads/*:refs/remotes/${remote}/*`],
195
164
  `Failed to fetch remote '${remote}'`,
196
165
  )
197
- const tip = yield* gitRevision(fs, checkout, "HEAD")
198
- const baseRevision = yield* gitRevision(
199
- fs,
200
- checkout,
201
- `refs/remotes/${remote}/${base}`,
166
+ const [tip, baseRevision] = yield* Effect.all(
167
+ [
168
+ gitRevision(fs, checkout, "HEAD"),
169
+ gitRevision(fs, checkout, `refs/remotes/${remote}/${base}`),
170
+ ],
171
+ { concurrency: "unbounded" },
202
172
  )
203
173
  if (!tip || !baseRevision) {
204
174
  return yield* new PushError({
@@ -239,12 +209,6 @@ const publishGit = (
239
209
  }
240
210
 
241
211
  onProgress?.("publish")
242
- yield* git(
243
- fs,
244
- checkout,
245
- ["push", remote, `HEAD:refs/heads/${branch}`],
246
- `Failed to push declared branch '${branch}'`,
247
- )
248
212
  yield* git(
249
213
  fs,
250
214
  checkout,
@@ -259,17 +223,13 @@ const publishGit = (
259
223
  fs,
260
224
  checkout,
261
225
  [
262
- "fetch",
226
+ "push",
227
+ "-u",
263
228
  remote,
264
- `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`,
229
+ `HEAD:refs/heads/${branch}`,
230
+ `--force-if-includes`,
265
231
  ],
266
- `Failed to refresh published branch '${branch}'`,
267
- )
268
- yield* git(
269
- fs,
270
- checkout,
271
- ["branch", "--set-upstream-to", `${remote}/${branch}`, branch],
272
- `Failed to establish upstream tracking for branch '${branch}'`,
232
+ `Failed to push declared branch '${branch}'`,
273
233
  )
274
234
  return { tip }
275
235
  })
@@ -403,10 +363,25 @@ const publishJj = (
403
363
  }
404
364
 
405
365
  const baseBookmark = `${base}@${remote}`
406
- const baseRevision = yield* optionalJjRevision(
407
- fs,
408
- checkout,
409
- `remote_bookmarks(exact:"${jjExact(base)}", exact:"${jjExact(remote)}")`,
366
+ const [baseRevision, localBookmark, remoteBookmark] = yield* Effect.all(
367
+ [
368
+ optionalJjRevision(
369
+ fs,
370
+ checkout,
371
+ `remote_bookmarks(exact:"${jjExact(base)}", exact:"${jjExact(remote)}")`,
372
+ ),
373
+ optionalJjRevision(
374
+ fs,
375
+ checkout,
376
+ `bookmarks(exact:"${jjExact(branch)}")`,
377
+ ),
378
+ optionalJjRevision(
379
+ fs,
380
+ checkout,
381
+ `remote_bookmarks(exact:"${jjExact(branch)}", exact:"${jjExact(remote)}")`,
382
+ ),
383
+ ],
384
+ { concurrency: "unbounded" },
410
385
  )
411
386
  if (!baseRevision) {
412
387
  return yield* new PushError({
@@ -432,28 +407,24 @@ const publishJj = (
432
407
  catch: (cause) => cause as PushError,
433
408
  })
434
409
 
435
- const localBookmark = yield* optionalJjRevision(
436
- fs,
437
- checkout,
438
- `bookmarks(exact:"${jjExact(branch)}")`,
439
- )
440
- if (
441
- localBookmark &&
442
- !(yield* jjAncestor(fs, checkout, localBookmark.commitId, tip.commitId))
443
- ) {
410
+ const [localBookmarkIsAncestor, remoteBookmarkIsAncestor] =
411
+ yield* Effect.all(
412
+ [
413
+ localBookmark
414
+ ? jjAncestor(fs, checkout, localBookmark.commitId, tip.commitId)
415
+ : Effect.succeed(true),
416
+ remoteBookmark
417
+ ? jjAncestor(fs, checkout, remoteBookmark.commitId, tip.commitId)
418
+ : Effect.succeed(true),
419
+ ],
420
+ { concurrency: "unbounded" },
421
+ )
422
+ if (!localBookmarkIsAncestor) {
444
423
  return yield* new PushError({
445
424
  message: `Local bookmark '${branch}' is not an ancestor of jj tip ${tip.changeId}; refusing to move it`,
446
425
  })
447
426
  }
448
- const remoteBookmark = yield* optionalJjRevision(
449
- fs,
450
- checkout,
451
- `remote_bookmarks(exact:"${jjExact(branch)}", exact:"${jjExact(remote)}")`,
452
- )
453
- if (
454
- remoteBookmark &&
455
- !(yield* jjAncestor(fs, checkout, remoteBookmark.commitId, tip.commitId))
456
- ) {
427
+ if (!remoteBookmarkIsAncestor) {
457
428
  return yield* new PushError({
458
429
  message: `Remote bookmark '${branch}@${remote}' is not an ancestor of jj tip ${tip.changeId}; refusing a non-fast-forward update`,
459
430
  })
@@ -0,0 +1,35 @@
1
+ export interface PushCommitMetadata {
2
+ readonly commitId: string
3
+ readonly changeId?: string
4
+ readonly description: string
5
+ readonly empty: boolean
6
+ readonly authorName: string
7
+ readonly authorEmail: string
8
+ readonly conflict: boolean
9
+ readonly parents: readonly string[]
10
+ }
11
+
12
+ export const parseGitCommits = (
13
+ output: string,
14
+ ): readonly PushCommitMetadata[] =>
15
+ output
16
+ .split("\x1e")
17
+ .map((record) => record.replace(/^\n+|\n+$/g, ""))
18
+ .filter(Boolean)
19
+ .map((record) => {
20
+ const [
21
+ commitId = "",
22
+ authorName = "",
23
+ authorEmail = "",
24
+ description = "",
25
+ ] = record.split("\0")
26
+ return {
27
+ commitId,
28
+ description,
29
+ empty: false,
30
+ authorName,
31
+ authorEmail,
32
+ conflict: false,
33
+ parents: [],
34
+ }
35
+ })