@markjaquith/agency 2.71.20 → 2.71.22

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.20",
3
+ "version": "2.71.22",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -21,8 +21,9 @@
21
21
  "index.ts",
22
22
  "cli.ts",
23
23
  "cli-main.ts",
24
- "pi-extensions",
25
24
  "src",
25
+ "pi-extensions",
26
+ "scripts/install-pi-extension.ts",
26
27
  "schemas",
27
28
  "fixtures/protocol",
28
29
  "README.md",
@@ -67,6 +68,7 @@
67
68
  "benchmark:act": "bun scripts/benchmark-act.ts",
68
69
  "benchmark:pr": "bun scripts/benchmark-pr.ts",
69
70
  "benchmark:release": "bun scripts/benchmark-release.ts",
71
+ "benchmark:repositories": "bun scripts/benchmark-repositories.ts",
70
72
  "benchmark:status": "bun scripts/benchmark-status.ts",
71
73
  "benchmark:next": "bun scripts/benchmark-next.ts",
72
74
  "benchmark:init": "bun scripts/benchmark-init.ts",
@@ -85,6 +87,7 @@
85
87
  "benchmark:task": "bun scripts/benchmark-task.ts",
86
88
  "benchmark:validate": "bun scripts/benchmark-validate.ts",
87
89
  "benchmark:worktree": "bun scripts/benchmark-worktree.ts",
90
+ "benchmark:review": "bun scripts/benchmark-review.ts",
88
91
  "test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
89
92
  "test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
90
93
  "format": "oxfmt",
@@ -0,0 +1,25 @@
1
+ import { cp, mkdir, rm } from "node:fs/promises"
2
+ import { homedir } from "node:os"
3
+ import { dirname, join } from "node:path"
4
+
5
+ export const piExtensionPath = (home = homedir()) =>
6
+ join(home, ".pi", "agent", "extensions", "agency.ts")
7
+
8
+ export const installPiExtension = async (
9
+ source = join(import.meta.dir, "..", "pi-extensions", "agency.ts"),
10
+ destination = piExtensionPath(),
11
+ ) => {
12
+ await mkdir(dirname(destination), { recursive: true })
13
+ await cp(source, destination)
14
+ }
15
+
16
+ export const uninstallPiExtension = async (destination = piExtensionPath()) => {
17
+ await rm(destination, { force: true })
18
+ }
19
+
20
+ if (import.meta.main) {
21
+ const command = process.argv[2] ?? "install"
22
+ if (command === "install") await installPiExtension()
23
+ else if (command === "uninstall") await uninstallPiExtension()
24
+ else throw new Error(`Unknown Pi extension lifecycle command: ${command}`)
25
+ }
@@ -520,8 +520,9 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
520
520
  ...Object.keys(config.repositories ?? {}),
521
521
  ...local.keys(),
522
522
  ])
523
- const repositories = yield* Effect.all(
524
- [...aliases].sort().map((alias) =>
523
+ return yield* Effect.forEach(
524
+ [...aliases].sort(),
525
+ (alias) =>
525
526
  Effect.gen(function* () {
526
527
  const path = join(reposPath, alias)
527
528
  const entry = local.get(alias)
@@ -575,10 +576,8 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
575
576
  states,
576
577
  } satisfies RepositoryInfo
577
578
  }),
578
- ),
579
- { concurrency: 4 },
579
+ { concurrency: 8 },
580
580
  )
581
- return repositories
582
581
  }),
583
582
 
584
583
  show: (alias: string, startPath: string = process.cwd()) =>
@@ -23,7 +23,10 @@ import {
23
23
  runLifecycleTransaction,
24
24
  type TransactionStep,
25
25
  } from "./LifecycleTransaction"
26
- import { RevisionConflictError } from "../workbase/document-revision"
26
+ import {
27
+ documentRevision,
28
+ RevisionConflictError,
29
+ } from "../workbase/document-revision"
27
30
  import {
28
31
  formatMarkdownDocument,
29
32
  parseFrontmatter,
@@ -359,24 +362,33 @@ export class ReviewService extends Effect.Service<ReviewService>()(
359
362
  message: `Cannot refresh review task '${taskId}'; its checkout is dirty or structurally unexpected`,
360
363
  })
361
364
  }
362
- const latest = yield* service.resolve(
365
+ const repository = yield* repositories.show(
363
366
  task.data.review.repo,
364
- task.data.review.source.kind === "pull-request"
365
- ? { pullRequest: task.data.review.source.url }
366
- : { ref: task.data.review.source.ref },
367
367
  root,
368
368
  )
369
+ if (!repository.remote || repository.states.includes("missing")) {
370
+ return yield* new ReviewError({
371
+ message: `Repository alias '${task.data.review.repo}' must be materialized with an origin remote`,
372
+ })
373
+ }
374
+ const versionControl = yield* VersionControlService
375
+ const backend = yield* versionControl.forWorkbase(root)
376
+ const latest = {
377
+ ...task.data.review,
378
+ commit: yield* fetchCommit(
379
+ repository.path,
380
+ task.data.review.source.kind === "pull-request"
381
+ ? task.data.review.source.fetchRef
382
+ : task.data.review.source.ref,
383
+ backend,
384
+ ),
385
+ refreshedAt: new Date().toISOString(),
386
+ } satisfies ReviewRecord
369
387
  const parsed = yield* parseFrontmatter(task.content, task.path)
370
388
  const content = formatMarkdownDocument(
371
389
  { ...task.data, review: latest },
372
390
  parsed.body,
373
391
  )
374
- const repository = yield* repositories.show(
375
- task.data.review.repo,
376
- root,
377
- )
378
- const versionControl = yield* VersionControlService
379
- const backend = yield* versionControl.forWorkbase(root)
380
392
  const gitEnvironment = yield* backend.gitEnvironment(
381
393
  repository.path,
382
394
  )
@@ -456,7 +468,7 @@ export class ReviewService extends Effect.Service<ReviewService>()(
456
468
  commit: latest.commit,
457
469
  changed: latest.commit !== task.data.review.commit,
458
470
  refreshedAt: latest.refreshedAt,
459
- revision: (yield* tasks.show(taskId, root)).revision,
471
+ revision: documentRevision(content),
460
472
  }
461
473
  }),
462
474
  )
@@ -594,12 +594,13 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
594
594
  }
595
595
  const content = yield* fs.readFile(path)
596
596
  const parsed = yield* parseFrontmatter(content, path)
597
+ const data: TaskData = yield* decodeTask(parsed.data)
597
598
  return {
598
599
  id: validId,
599
600
  path,
600
601
  content,
601
602
  revision: documentRevision(content),
602
- data: yield* decodeTask(parsed.data),
603
+ data,
603
604
  } satisfies TaskRecord
604
605
  }),
605
606
 
@@ -1,9 +1,13 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test"
2
- import { Effect } from "effect"
2
+ import { Effect, Layer } from "effect"
3
3
  import { mkdir, realpath, stat } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
- import { VersionControlService } from "./VersionControlService"
6
+ import {
7
+ GitVersionControlService,
8
+ VersionControlService,
9
+ } from "./VersionControlService"
10
+ import { FileSystemService } from "./FileSystemService"
7
11
  import { preferredVersionControl } from "../workbase/version-control"
8
12
 
9
13
  const run = async (args: string[], cwd?: string) => {
@@ -24,6 +28,39 @@ describe("VersionControlService", () => {
24
28
  expect(preferredVersionControl(() => null)).toBe("git")
25
29
  })
26
30
 
31
+ test("inspects a Git repository with one subprocess", async () => {
32
+ const commands: readonly string[][] = []
33
+ const fileSystem = {
34
+ ...FileSystemService.Default,
35
+ runCommand: (command: readonly string[]) => {
36
+ ;(commands as string[][]).push([...command])
37
+ return Effect.succeed({
38
+ exitCode: 0,
39
+ stdout:
40
+ "local\tcore.bare true\nlocal\tremote.origin.url agency:agency.git\nglobal\turl.https://example.com/.insteadof agency:\n",
41
+ stderr: "",
42
+ })
43
+ },
44
+ } as unknown as Effect.Effect.Success<typeof FileSystemService>
45
+ const backend = await Effect.runPromise(
46
+ GitVersionControlService.pipe(
47
+ Effect.provide(GitVersionControlService.Default),
48
+ ),
49
+ )
50
+ const inspection = await Effect.runPromise(
51
+ backend
52
+ .inspectRepository("/repository")
53
+ .pipe(Effect.provide(Layer.succeed(FileSystemService, fileSystem))),
54
+ )
55
+
56
+ expect(commands).toHaveLength(1)
57
+ expect(commands[0]).toContain("--get-regexp")
58
+ expect(inspection).toEqual({
59
+ kind: "bare",
60
+ remote: "https://example.com/agency.git",
61
+ })
62
+ })
63
+
27
64
  test("selects the backend persisted by the workbase", async () => {
28
65
  for (const kind of ["git", "jj"] as const) {
29
66
  const root = await createTempDir()
@@ -118,6 +118,52 @@ const requireSuccess = (
118
118
  ),
119
119
  )
120
120
 
121
+ interface GitConfigEntry {
122
+ readonly scope: string
123
+ readonly key: string
124
+ readonly value: string
125
+ }
126
+
127
+ const parseGitConfig = (output: string): readonly GitConfigEntry[] =>
128
+ output
129
+ .trim()
130
+ .split("\n")
131
+ .filter(Boolean)
132
+ .flatMap((line) => {
133
+ const scopeSeparator = line.indexOf("\t")
134
+ const valueSeparator = line.indexOf(" ", scopeSeparator + 1)
135
+ return scopeSeparator >= 0 && valueSeparator >= 0
136
+ ? [
137
+ {
138
+ scope: line.slice(0, scopeSeparator),
139
+ key: line.slice(scopeSeparator + 1, valueSeparator),
140
+ value: line.slice(valueSeparator + 1),
141
+ },
142
+ ]
143
+ : []
144
+ })
145
+
146
+ const expandGitUrl = (
147
+ url: string,
148
+ entries: readonly GitConfigEntry[],
149
+ ): string => {
150
+ let replacement: { readonly prefix: string; readonly base: string } | null =
151
+ null
152
+ for (const entry of entries) {
153
+ const match = /^url\.(.*)\.insteadof$/i.exec(entry.key)
154
+ if (
155
+ match?.[1] !== undefined &&
156
+ url.startsWith(entry.value) &&
157
+ (replacement === null || entry.value.length > replacement.prefix.length)
158
+ ) {
159
+ replacement = { prefix: entry.value, base: match[1] }
160
+ }
161
+ }
162
+ return replacement === null
163
+ ? url
164
+ : replacement.base + url.slice(replacement.prefix.length)
165
+ }
166
+
121
167
  const parseGitWorktrees = (output: string): readonly RegisteredWorkspace[] => {
122
168
  const workspaces: RegisteredWorkspace[] = []
123
169
  let current: RegisteredWorkspace | null = null
@@ -168,22 +214,33 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
168
214
  inspectRepository: (path) =>
169
215
  Effect.gen(function* () {
170
216
  const fs = yield* FileSystemService
171
- const valid = yield* fs.runCommand(
172
- ["git", "-C", path, "rev-parse", "--git-dir"],
173
- { captureOutput: true },
174
- )
175
- if (valid.exitCode !== 0) return null
176
- const bare = yield* fs.runCommand(
177
- ["git", "-C", path, "rev-parse", "--is-bare-repository"],
178
- { captureOutput: true },
179
- )
180
- const remote = yield* fs.runCommand(
181
- ["git", "-C", path, "remote", "get-url", "origin"],
217
+ const inspection = yield* fs.runCommand(
218
+ [
219
+ "git",
220
+ "-C",
221
+ path,
222
+ "config",
223
+ "--show-scope",
224
+ "--get-regexp",
225
+ "^(core\\.bare|remote\\.origin\\.url|url\\..*\\.insteadof)$",
226
+ ],
182
227
  { captureOutput: true },
183
228
  )
229
+ if (inspection.exitCode !== 0) return null
230
+ const entries = parseGitConfig(inspection.stdout)
231
+ const bare = entries.find(
232
+ (entry) =>
233
+ (entry.scope === "local" || entry.scope === "worktree") &&
234
+ entry.key === "core.bare",
235
+ )
236
+ if (!bare) return null
237
+ const remote = entries.find(
238
+ (entry) => entry.key === "remote.origin.url",
239
+ )?.value
184
240
  return {
185
- kind: bare.stdout.trim() === "true" ? "bare" : "repository",
186
- remote: remote.exitCode === 0 ? remote.stdout.trim() : null,
241
+ kind: bare.value === "true" ? "bare" : "repository",
242
+ remote:
243
+ remote === undefined ? null : expandGitUrl(remote, entries),
187
244
  } as const
188
245
  }),
189
246
  gitEnvironment: () => Effect.succeed({}),