@markjaquith/agency 2.73.1 → 2.73.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.73.1",
3
+ "version": "2.73.2",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -212,6 +212,13 @@ status: working
212
212
  "main",
213
213
  ])
214
214
  await Bun.write(join(checkout, "dirty.txt"), "preserved\n")
215
+ await runGit([
216
+ "-C",
217
+ target,
218
+ "update-ref",
219
+ "refs/agency/reviews/active",
220
+ "main",
221
+ ])
215
222
 
216
223
  const result = await runTestEffect(
217
224
  RepositoryService.pipe(
@@ -238,6 +245,96 @@ status: working
238
245
  expect(await Bun.file(join(root, "tasks/active/TASK.md")).text()).toContain(
239
246
  "repo: linked",
240
247
  )
248
+ expect(
249
+ await gitOutput([
250
+ "--git-dir",
251
+ join(root, "repos/linked"),
252
+ "rev-parse",
253
+ "refs/agency/reviews/active",
254
+ ]),
255
+ ).toBe(await gitOutput(["-C", target, "rev-parse", "main"]))
256
+ })
257
+
258
+ test("preserves detached registered worktree commits from a shallow linked repository", async () => {
259
+ const upstream = join(root, "upstream")
260
+ const target = join(root, "shallow-linked-repository")
261
+ const checkout = join(root, "tasks/detached/code/linked")
262
+ await runGit(["init", "--bare", "--initial-branch=main", upstream])
263
+ const seed = join(root, "seed")
264
+ await runGit(["clone", upstream, seed])
265
+ await runGit(["-C", seed, "config", "user.email", "test@example.com"])
266
+ await runGit(["-C", seed, "config", "user.name", "Test"])
267
+ await Bun.write(join(seed, "README.md"), "shallow\n")
268
+ await runGit(["-C", seed, "add", "README.md"])
269
+ await runGit(["-C", seed, "commit", "-m", "initial"])
270
+ await runGit(["-C", seed, "push", "origin", "main"])
271
+ await runGit(["clone", "--depth=1", `file://${upstream}`, target])
272
+ await runGit(["-C", target, "config", "user.email", "test@example.com"])
273
+ await runGit(["-C", target, "config", "user.name", "Test"])
274
+ await runGit([
275
+ "-C",
276
+ target,
277
+ "remote",
278
+ "set-url",
279
+ "origin",
280
+ portableRemote("shallow-materialized"),
281
+ ])
282
+ await mkdir(dirname(checkout), { recursive: true })
283
+ await runGit([
284
+ "-C",
285
+ target,
286
+ "worktree",
287
+ "add",
288
+ "--detach",
289
+ checkout,
290
+ "main",
291
+ ])
292
+ await Bun.write(join(checkout, "detached.txt"), "preserved\n")
293
+ await runGit(["-C", checkout, "add", "detached.txt"])
294
+ await runGit(["-C", checkout, "commit", "-m", "detached work"])
295
+ const detachedCommit = await gitOutput([
296
+ "-C",
297
+ checkout,
298
+ "rev-parse",
299
+ "HEAD",
300
+ ])
301
+ await runTestEffect(
302
+ RepositoryService.pipe(
303
+ Effect.flatMap((service) => service.link("linked", target, root)),
304
+ ),
305
+ )
306
+
307
+ await runTestEffect(
308
+ RepositoryService.pipe(
309
+ Effect.flatMap((service) => service.materialize("linked", root)),
310
+ ),
311
+ )
312
+
313
+ expect(await gitOutput(["-C", checkout, "rev-parse", "HEAD"])).toBe(
314
+ detachedCommit,
315
+ )
316
+ expect(await Bun.file(join(checkout, "detached.txt")).text()).toBe(
317
+ "preserved\n",
318
+ )
319
+ expect(
320
+ await gitOutput(["-C", checkout, "rev-parse", "--git-common-dir"]),
321
+ ).toBe(await realpath(join(root, "repos/linked")))
322
+ await runGit([
323
+ "--git-dir",
324
+ join(root, "repos/linked"),
325
+ "cat-file",
326
+ "-e",
327
+ `${detachedCommit}^{commit}`,
328
+ ])
329
+ expect(
330
+ await gitOutput([
331
+ "--git-dir",
332
+ join(root, "repos/linked"),
333
+ "for-each-ref",
334
+ "--format=%(refname)",
335
+ "refs/agency/materialize/",
336
+ ]),
337
+ ).toBe("")
241
338
  })
242
339
 
243
340
  test("materializes an alias linked through an existing worktree", async () => {
@@ -19,6 +19,7 @@ import {
19
19
  import { documentRevision } from "../workbase/document-revision"
20
20
  import {
21
21
  VersionControlService,
22
+ type RegisteredWorkspace,
22
23
  type VersionControlBackend,
23
24
  } from "./VersionControlService"
24
25
 
@@ -558,9 +559,9 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
558
559
 
559
560
  const source = yield* fs.realPath(repository.path)
560
561
  const registered = yield* backend.listWorkspaces(repository.path)
561
- const registeredPaths = registered
562
- .map((workspace) => workspace.path)
563
- .sort()
562
+ const registeredState = registered
563
+ .map(({ path, commit, branch }) => ({ path, commit, branch }))
564
+ .sort((left, right) => left.path.localeCompare(right.path))
564
565
  const worktrees: (typeof registered)[number][] = []
565
566
  for (const workspace of registered) {
566
567
  if (!(yield* fs.isDirectory(workspace.path))) {
@@ -634,6 +635,77 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
634
635
  ),
635
636
  ),
636
637
  )
638
+ const reviewRefs = yield* fs.runCommand(
639
+ [
640
+ "git",
641
+ "-C",
642
+ source,
643
+ "for-each-ref",
644
+ "--format=%(refname)",
645
+ "refs/agency/reviews/",
646
+ ],
647
+ { captureOutput: true },
648
+ )
649
+ if (reviewRefs.exitCode !== 0) {
650
+ yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
651
+ return yield* new RepositoryError({
652
+ message: `Failed to enumerate Agency review refs while materializing repository '${alias}'`,
653
+ })
654
+ }
655
+ const preservedReviewRefs = reviewRefs.stdout
656
+ .split("\n")
657
+ .map((ref) => ref.trim())
658
+ .filter(Boolean)
659
+ const worktreeCommits = [
660
+ ...new Set(
661
+ worktrees
662
+ .map((workspace) => workspace.commit)
663
+ .filter((commit): commit is string => commit !== null),
664
+ ),
665
+ ]
666
+ const temporaryRefs = worktreeCommits.map(
667
+ (_, index) =>
668
+ `refs/agency/materialize/${suffix}/${String(index).padStart(4, "0")}`,
669
+ )
670
+ const preservationRefspecs = [
671
+ ...preservedReviewRefs.map((ref) => `+${ref}:${ref}`),
672
+ ...worktreeCommits.map(
673
+ (commit, index) => `+${commit}:${temporaryRefs[index]}`,
674
+ ),
675
+ ]
676
+ if (preservationRefspecs.length > 0) {
677
+ const preserved = yield* fs.runCommand(
678
+ [
679
+ "git",
680
+ "--git-dir",
681
+ staging,
682
+ "fetch",
683
+ "--no-tags",
684
+ "--no-write-fetch-head",
685
+ source,
686
+ ...preservationRefspecs,
687
+ ],
688
+ { captureOutput: true },
689
+ )
690
+ if (preserved.exitCode !== 0) {
691
+ yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
692
+ return yield* new RepositoryError({
693
+ message: `Failed to preserve Agency refs and registered worktree commits while materializing repository '${alias}': ${preserved.stderr.trim()}`,
694
+ })
695
+ }
696
+ for (const ref of temporaryRefs) {
697
+ const removed = yield* fs.runCommand(
698
+ ["git", "--git-dir", staging, "update-ref", "-d", ref],
699
+ { captureOutput: true },
700
+ )
701
+ if (removed.exitCode !== 0) {
702
+ yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
703
+ return yield* new RepositoryError({
704
+ message: `Failed to remove temporary materialization ref '${ref}' for repository '${alias}': ${removed.stderr.trim()}`,
705
+ })
706
+ }
707
+ }
708
+ }
637
709
  yield* backend
638
710
  .setRemoteUrl(staging, "origin", repository.declaredRemote)
639
711
  .pipe(
@@ -728,16 +800,16 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
728
800
  .pipe(
729
801
  Effect.provideService(FileSystemService, fs),
730
802
  ) as unknown as Effect.Effect<
731
- readonly { readonly path: string }[],
803
+ readonly RegisteredWorkspace[],
732
804
  unknown,
733
805
  never
734
806
  >,
735
807
  )
736
- const currentPaths = current
737
- .map((workspace) => workspace.path)
738
- .sort()
808
+ const currentState = current
809
+ .map(({ path, commit, branch }) => ({ path, commit, branch }))
810
+ .sort((left, right) => left.path.localeCompare(right.path))
739
811
  if (
740
- JSON.stringify(currentPaths) !== JSON.stringify(registeredPaths)
812
+ JSON.stringify(currentState) !== JSON.stringify(registeredState)
741
813
  )
742
814
  throw new Error(
743
815
  `Git worktree registrations changed during materialization`,