@markjaquith/agency 3.2.4 → 3.2.5

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.4",
3
+ "version": "3.2.5",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -1,6 +1,6 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
2
2
  import { Effect } from "effect"
3
- import { mkdir, realpath } from "node:fs/promises"
3
+ import { mkdir, realpath, stat } from "node:fs/promises"
4
4
  import { dirname, join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
6
  import { RepositoryService } from "./RepositoryService"
@@ -136,6 +136,30 @@ describe("RepositoryService", () => {
136
136
  })
137
137
  })
138
138
 
139
+ test("inspects only the requested repository alias", async () => {
140
+ const requested = join(root, "repos/requested")
141
+ const unrelated = join(root, "repos/unrelated")
142
+ await mkdir(requested, { recursive: true })
143
+ await mkdir(unrelated, { recursive: true })
144
+ await runGit(["init", "--initial-branch=main", requested])
145
+ await runGit(["init", "--initial-branch=main", unrelated])
146
+
147
+ const spawn = spyOn(Bun, "spawn")
148
+ try {
149
+ const repository = await runTestEffect(
150
+ RepositoryService.pipe(
151
+ Effect.flatMap((service) => service.show("requested", root)),
152
+ ),
153
+ )
154
+ expect(repository.alias).toBe("requested")
155
+ expect(spawn.mock.calls).toHaveLength(1)
156
+ expect(spawn.mock.calls[0]?.[0]).toContain(requested)
157
+ expect(spawn.mock.calls[0]?.[0]).not.toContain(unrelated)
158
+ } finally {
159
+ spawn.mockRestore()
160
+ }
161
+ })
162
+
139
163
  test("materializes a linked alias without invalidating active worktrees", async () => {
140
164
  const target = join(root, "linked-repository")
141
165
  const checkout = join(root, "tasks/active/code/linked")
@@ -146,6 +170,13 @@ describe("RepositoryService", () => {
146
170
  await Bun.write(join(target, "README.md"), "linked\n")
147
171
  await runGit(["-C", target, "add", "README.md"])
148
172
  await runGit(["-C", target, "commit", "-m", "initial"])
173
+ const commit = await gitOutput(["-C", target, "rev-parse", "HEAD"])
174
+ const sourceObject = join(
175
+ target,
176
+ ".git/objects",
177
+ commit.slice(0, 2),
178
+ commit.slice(2),
179
+ )
149
180
  await setPortableOrigin(target, "materialized")
150
181
  await runTestEffect(
151
182
  RepositoryService.pipe(
@@ -218,6 +249,15 @@ status: working
218
249
  "refs/agency/reviews/active",
219
250
  ]),
220
251
  ).toBe(await gitOutput(["-C", target, "rev-parse", "main"]))
252
+ const materializedObject = join(
253
+ root,
254
+ "repos/linked/objects",
255
+ commit.slice(0, 2),
256
+ commit.slice(2),
257
+ )
258
+ expect((await stat(materializedObject)).ino).not.toBe(
259
+ (await stat(sourceObject)).ino,
260
+ )
221
261
  })
222
262
 
223
263
  test("preserves detached registered worktree commits from a shallow linked repository", async () => {
@@ -186,18 +186,71 @@ const portableRemote = (path: string, backend?: VersionControlBackend) =>
186
186
  return yield* validateRemote(remote)
187
187
  })
188
188
 
189
- const find = (alias: string, startPath: string) =>
189
+ const inspectRepository = (
190
+ alias: string,
191
+ state: Effect.Effect.Success<ReturnType<typeof configState>>,
192
+ backend: VersionControlBackend,
193
+ ) =>
190
194
  Effect.gen(function* () {
191
- const service = yield* RepositoryService
192
- const validAlias = yield* validateAlias(alias)
193
- const repositories = yield* service.list(startPath)
194
- const repository = repositories.find((item) => item.alias === validAlias)
195
- if (!repository) {
195
+ const fs = yield* FileSystemService
196
+ const path = join(state.root, "repos", alias)
197
+ const declaredRemote = state.config.repositories?.[alias]?.remote ?? null
198
+ const entry = yield* fs.inspectFile(path)
199
+ if (entry.kind === "missing") {
200
+ if (declaredRemote) {
201
+ return {
202
+ alias,
203
+ path,
204
+ kind: null,
205
+ remote: null,
206
+ declaredRemote,
207
+ target: null,
208
+ states: ["declared", "missing"],
209
+ } as RepositoryInfo
210
+ }
196
211
  return yield* new RepositoryError({
197
- message: `Unknown repository alias '${validAlias}'`,
212
+ message: `Unknown repository alias '${alias}'`,
198
213
  })
199
214
  }
200
- return repository
215
+ const isSymlink = entry.kind === "symlink"
216
+ if (!isSymlink && !(yield* fs.isDirectory(path))) {
217
+ return {
218
+ alias,
219
+ path,
220
+ kind: null,
221
+ remote: null,
222
+ declaredRemote,
223
+ target: null,
224
+ states: [...(declaredRemote ? (["declared"] as const) : []), "invalid"],
225
+ } as RepositoryInfo
226
+ }
227
+ const target = isSymlink ? yield* fs.readSymlinkTarget(path) : null
228
+ const inspection = yield* backend.inspectRepository(path)
229
+ const remote = inspection?.remote ?? null
230
+ const states: RepositoryState[] = []
231
+ if (declaredRemote) states.push("declared")
232
+ states.push(isSymlink ? "linked" : "materialized")
233
+ if (!inspection) states.push("invalid")
234
+ if (declaredRemote && remote !== declaredRemote)
235
+ states.push("remote-drifted")
236
+ return {
237
+ alias,
238
+ path,
239
+ kind: isSymlink ? "symlink" : (inspection?.kind ?? "repository"),
240
+ remote,
241
+ declaredRemote,
242
+ target,
243
+ states,
244
+ } as RepositoryInfo
245
+ })
246
+
247
+ const find = (alias: string, startPath: string) =>
248
+ Effect.gen(function* () {
249
+ const versionControl = yield* VersionControlService
250
+ const validAlias = yield* validateAlias(alias)
251
+ const state = yield* configState(startPath)
252
+ const backend = yield* versionControl.forWorkbase(state.root)
253
+ return yield* inspectRepository(validAlias, state, backend)
201
254
  })
202
255
 
203
256
  const requireMaterialized = (repository: RepositoryInfo) =>
@@ -450,14 +503,11 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
450
503
  const backend = yield* versionControl.forWorkbase(state.root)
451
504
  const destination = join(state.root, "repos", validAlias)
452
505
  const resolvedTarget = resolve(startPath, target)
453
- const existing = (yield* RepositoryService)
454
- .list(state.root)
455
- .pipe(
456
- Effect.map((items) =>
457
- items.find((item) => item.alias === validAlias),
458
- ),
459
- )
460
- const current = yield* existing
506
+ const current =
507
+ state.config.repositories?.[validAlias] ||
508
+ (yield* fs.exists(destination))
509
+ ? yield* inspectRepository(validAlias, state, backend)
510
+ : undefined
461
511
  const localCurrent =
462
512
  current && !current.states.includes("missing") ? current : undefined
463
513
  if (localCurrent?.kind === "symlink") {
@@ -521,10 +571,17 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
521
571
  Effect.gen(function* () {
522
572
  const fs = yield* FileSystemService
523
573
  const versionControl = yield* VersionControlService
524
- const repository = yield* find(alias, startPath)
574
+ const validAlias = yield* validateAlias(alias)
575
+ const state = yield* configState(startPath)
576
+ const backend = yield* versionControl.forWorkbase(state.root)
577
+ const repository = yield* inspectRepository(
578
+ validAlias,
579
+ state,
580
+ backend,
581
+ )
525
582
  if (repository.kind !== "symlink" || !repository.target) {
526
583
  return yield* new RepositoryError({
527
- message: `Repository alias '${alias}' is not linked`,
584
+ message: `Repository alias '${validAlias}' is not linked`,
528
585
  })
529
586
  }
530
587
  if (repository.states.includes("invalid")) {
@@ -543,9 +600,6 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
543
600
  })
544
601
  }
545
602
 
546
- const state = yield* configState(startPath)
547
- const backend = yield* versionControl.forWorkbase(state.root)
548
-
549
603
  const source = yield* fs.realPath(repository.path)
550
604
  const registered = yield* backend.listWorkspaces(repository.path)
551
605
  const registeredState = registered
@@ -854,78 +908,22 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
854
908
  list: (startPath: string = process.cwd()) =>
855
909
  Effect.gen(function* () {
856
910
  const fs = yield* FileSystemService
857
- const { root, config } = yield* WorkbaseService.pipe(
858
- Effect.flatMap((service) => service.loadConfig(startPath)),
859
- )
860
911
  const versionControl = yield* VersionControlService
861
- const backend = yield* versionControl.forWorkbase(root)
862
- const reposPath = join(root, "repos")
912
+ const state = yield* configState(startPath)
913
+ const backend = yield* versionControl.forWorkbase(state.root)
914
+ const reposPath = join(state.root, "repos")
863
915
  const entries = (yield* fs.isDirectory(reposPath))
864
916
  ? (yield* fs.readDirectory(reposPath)).filter(
865
917
  (entry) => !entry.name.startsWith(".agency-"),
866
918
  )
867
919
  : []
868
- const local = new Map(entries.map((entry) => [entry.name, entry]))
869
920
  const aliases = new Set([
870
- ...Object.keys(config.repositories ?? {}),
871
- ...local.keys(),
921
+ ...Object.keys(state.config.repositories ?? {}),
922
+ ...entries.map((entry) => entry.name),
872
923
  ])
873
924
  return yield* Effect.forEach(
874
925
  [...aliases].sort(),
875
- (alias) =>
876
- Effect.gen(function* () {
877
- const path = join(reposPath, alias)
878
- const entry = local.get(alias)
879
- const declaredRemote =
880
- config.repositories?.[alias]?.remote ?? null
881
- if (!entry) {
882
- return {
883
- alias,
884
- path,
885
- kind: null,
886
- remote: null,
887
- declaredRemote,
888
- target: null,
889
- states: ["declared", "missing"] as RepositoryState[],
890
- } satisfies RepositoryInfo
891
- }
892
- if (!entry.isDirectory && !entry.isSymlink) {
893
- return {
894
- alias,
895
- path,
896
- kind: null,
897
- remote: null,
898
- declaredRemote,
899
- target: null,
900
- states: [
901
- ...(declaredRemote ? (["declared"] as const) : []),
902
- "invalid",
903
- ] as RepositoryState[],
904
- } satisfies RepositoryInfo
905
- }
906
- const target = entry.isSymlink
907
- ? yield* fs.readSymlinkTarget(path)
908
- : null
909
- const inspection = yield* backend.inspectRepository(path)
910
- const remote = inspection?.remote ?? null
911
- const states: RepositoryState[] = []
912
- if (declaredRemote) states.push("declared")
913
- states.push(entry.isSymlink ? "linked" : "materialized")
914
- if (!inspection) states.push("invalid")
915
- if (declaredRemote && remote !== declaredRemote)
916
- states.push("remote-drifted")
917
- return {
918
- alias,
919
- path,
920
- kind: entry.isSymlink
921
- ? "symlink"
922
- : (inspection?.kind ?? "repository"),
923
- remote,
924
- declaredRemote,
925
- target,
926
- states,
927
- } satisfies RepositoryInfo
928
- }),
926
+ (alias) => inspectRepository(alias, state, backend),
929
927
  { concurrency: 8 },
930
928
  )
931
929
  }),
@@ -171,7 +171,15 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
171
171
  yield* requireSuccess(
172
172
  "Failed to clone Git repository",
173
173
  fs.runCommand(
174
- ["git", "clone", "--bare", "--", source, destination],
174
+ [
175
+ "git",
176
+ "clone",
177
+ "--bare",
178
+ "--no-hardlinks",
179
+ "--",
180
+ source,
181
+ destination,
182
+ ],
175
183
  {
176
184
  captureOutput: true,
177
185
  },