@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 +5 -2
- package/scripts/install-pi-extension.ts +25 -0
- package/src/services/RepositoryService.ts +4 -5
- package/src/services/ReviewService.ts +24 -12
- package/src/services/TaskService.ts +2 -1
- package/src/services/VersionControlService.test.ts +39 -2
- package/src/services/VersionControlService.ts +70 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.71.
|
|
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
|
-
|
|
524
|
-
[...aliases].sort()
|
|
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 {
|
|
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
|
|
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: (
|
|
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
|
|
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 {
|
|
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
|
|
172
|
-
[
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
186
|
-
remote:
|
|
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({}),
|