@markjaquith/agency 2.50.0 → 2.52.0
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/README.md +34 -8
- package/cli.ts +33 -1
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +1 -0
- package/src/cli-parser.test.ts +19 -0
- package/src/cli-parser.ts +23 -0
- package/src/cli.test.ts +4 -4
- package/src/commands/init.test.ts +2 -0
- package/src/commands/vcs.test.ts +75 -0
- package/src/commands/vcs.ts +72 -0
- package/src/commands/worktree.ts +1 -1
- package/src/graph-schema.test.ts +1 -1
- package/src/graph-schema.ts +1 -0
- package/src/services/ArchiveService.ts +23 -0
- package/src/services/ContextService.ts +62 -13
- package/src/services/DoctorService.ts +13 -6
- package/src/services/GraphService.ts +1 -0
- package/src/services/PhaseService.ts +191 -70
- package/src/services/PullRequestService.ts +14 -30
- package/src/services/RepositoryService.test.ts +31 -1
- package/src/services/RepositoryService.ts +63 -31
- package/src/services/ReviewService.ts +23 -0
- package/src/services/SyncService.test.ts +67 -0
- package/src/services/SyncService.ts +51 -84
- package/src/services/TaskPhaseService.test.ts +80 -0
- package/src/services/VcsMigrationService.test.ts +245 -0
- package/src/services/VcsMigrationService.ts +812 -0
- package/src/services/VersionControlService.test.ts +100 -0
- package/src/services/VersionControlService.ts +479 -0
- package/src/services/WorkbaseService.ts +5 -1
- package/src/services/WorktreeService.test.ts +72 -1
- package/src/services/WorktreeService.ts +808 -308
- package/src/test-utils.ts +10 -0
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/schemas.ts +1 -0
- package/src/workbase/version-control.ts +5 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { lstat, mkdir } from "node:fs/promises"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
|
+
import { ClaimService } from "./ClaimService"
|
|
7
|
+
import { TaskService } from "./TaskService"
|
|
8
|
+
import { VcsMigrationService } from "./VcsMigrationService"
|
|
9
|
+
import { WorktreeService } from "./WorktreeService"
|
|
10
|
+
|
|
11
|
+
const run = async (args: string[], cwd?: string) => {
|
|
12
|
+
const child = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" })
|
|
13
|
+
const exitCode = await child.exited
|
|
14
|
+
if (exitCode !== 0) throw new Error(await new Response(child.stderr).text())
|
|
15
|
+
return new Response(child.stdout).text()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const exists = async (path: string) => {
|
|
19
|
+
try {
|
|
20
|
+
await lstat(path)
|
|
21
|
+
return true
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("VcsMigrationService", () => {
|
|
28
|
+
let root: string
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
root = await createTempDir()
|
|
32
|
+
await Bun.write(
|
|
33
|
+
join(root, "agency.json"),
|
|
34
|
+
JSON.stringify({ version: 2, vcs: "git" }),
|
|
35
|
+
)
|
|
36
|
+
const source = join(root, "source")
|
|
37
|
+
await mkdir(source)
|
|
38
|
+
await run(["git", "init", "--initial-branch=main"], source)
|
|
39
|
+
await run(["git", "config", "user.email", "test@example.com"], source)
|
|
40
|
+
await run(["git", "config", "user.name", "Test"], source)
|
|
41
|
+
await Bun.write(join(source, "README.md"), "example\n")
|
|
42
|
+
await run(["git", "add", "README.md"], source)
|
|
43
|
+
await run(["git", "commit", "-m", "initial"], source)
|
|
44
|
+
await mkdir(join(root, "repos"))
|
|
45
|
+
await run(["git", "clone", "--bare", source, join(root, "repos/agency")])
|
|
46
|
+
await run(["git", "clone", "--bare", source, join(root, "repos/effect")])
|
|
47
|
+
await runTestEffect(
|
|
48
|
+
TaskService.pipe(
|
|
49
|
+
Effect.flatMap((service) =>
|
|
50
|
+
service.create(
|
|
51
|
+
{
|
|
52
|
+
id: "example",
|
|
53
|
+
ticketUrl: null,
|
|
54
|
+
repo: "agency",
|
|
55
|
+
repos: [{ repo: "effect", ref: "main" }],
|
|
56
|
+
branch: "task/example",
|
|
57
|
+
base: "main",
|
|
58
|
+
},
|
|
59
|
+
root,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
await runTestEffect(
|
|
65
|
+
WorktreeService.pipe(
|
|
66
|
+
Effect.flatMap((service) =>
|
|
67
|
+
service.materialize("example", undefined, root),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterEach(async () => cleanupTempDir(root))
|
|
74
|
+
|
|
75
|
+
test("migrates Git worktrees to jj workspaces and back", async () => {
|
|
76
|
+
if (!Bun.which("jj")) return
|
|
77
|
+
const checkout = join(root, "tasks/example/code/agency")
|
|
78
|
+
const reference = join(root, "tasks/example/code/effect")
|
|
79
|
+
const dryRun = await runTestEffect(
|
|
80
|
+
VcsMigrationService.pipe(
|
|
81
|
+
Effect.flatMap((service) => service.migrate("jj", root)),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
expect(dryRun).toMatchObject({
|
|
85
|
+
source: "git",
|
|
86
|
+
target: "jj",
|
|
87
|
+
mode: "dry-run",
|
|
88
|
+
workspaceCount: 2,
|
|
89
|
+
})
|
|
90
|
+
expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("git")
|
|
91
|
+
|
|
92
|
+
const migrated = await runTestEffect(
|
|
93
|
+
VcsMigrationService.pipe(
|
|
94
|
+
Effect.flatMap((service) =>
|
|
95
|
+
service.migrate("jj", root, { apply: true }),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
expect(migrated.mode).toBe("apply")
|
|
100
|
+
expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("jj")
|
|
101
|
+
expect(await exists(join(root, "repos/agency/.jj"))).toBe(true)
|
|
102
|
+
expect(await exists(join(root, "repos/effect/.jj"))).toBe(true)
|
|
103
|
+
expect(await exists(join(checkout, ".jj"))).toBe(true)
|
|
104
|
+
expect(await exists(join(reference, ".jj"))).toBe(true)
|
|
105
|
+
expect(
|
|
106
|
+
await run(["jj", "-R", checkout, "log", "--no-graph", "-r", "@-"]),
|
|
107
|
+
).toContain("initial")
|
|
108
|
+
expect(
|
|
109
|
+
(
|
|
110
|
+
await runTestEffect(
|
|
111
|
+
WorktreeService.pipe(
|
|
112
|
+
Effect.flatMap((service) =>
|
|
113
|
+
service.inspect("example", undefined, root),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
).conflicts,
|
|
118
|
+
).toEqual([])
|
|
119
|
+
|
|
120
|
+
await runTestEffect(
|
|
121
|
+
VcsMigrationService.pipe(
|
|
122
|
+
Effect.flatMap((service) =>
|
|
123
|
+
service.migrate("git", root, { apply: true }),
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
expect((await Bun.file(join(root, "agency.json")).json()).vcs).toBe("git")
|
|
128
|
+
expect(await exists(join(root, "repos/agency/.jj"))).toBe(false)
|
|
129
|
+
expect(await exists(join(root, "repos/effect/.jj"))).toBe(false)
|
|
130
|
+
expect(await exists(join(checkout, ".git"))).toBe(true)
|
|
131
|
+
expect(await exists(join(reference, ".git"))).toBe(true)
|
|
132
|
+
expect(
|
|
133
|
+
(await run(["git", "-C", checkout, "branch", "--show-current"])).trim(),
|
|
134
|
+
).toBe("task/example")
|
|
135
|
+
expect(
|
|
136
|
+
(
|
|
137
|
+
await run(["git", "-C", reference, "rev-parse", "--abbrev-ref", "HEAD"])
|
|
138
|
+
).trim(),
|
|
139
|
+
).toBe("HEAD")
|
|
140
|
+
expect(
|
|
141
|
+
(
|
|
142
|
+
await runTestEffect(
|
|
143
|
+
WorktreeService.pipe(
|
|
144
|
+
Effect.flatMap((service) =>
|
|
145
|
+
service.inspect("example", undefined, root),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
).conflicts,
|
|
150
|
+
).toEqual([])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test("allows clean unclaimed working workspaces", async () => {
|
|
154
|
+
if (!Bun.which("jj")) return
|
|
155
|
+
await runTestEffect(
|
|
156
|
+
TaskService.pipe(
|
|
157
|
+
Effect.flatMap((service) =>
|
|
158
|
+
service.setStatus("example", "working", root),
|
|
159
|
+
),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
const migrated = await runTestEffect(
|
|
163
|
+
VcsMigrationService.pipe(
|
|
164
|
+
Effect.flatMap((service) =>
|
|
165
|
+
service.migrate("jj", root, { apply: true }),
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
expect(migrated.mode).toBe("apply")
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test("blocks active claims", async () => {
|
|
173
|
+
if (!Bun.which("jj")) return
|
|
174
|
+
const inspected = await runTestEffect(
|
|
175
|
+
ClaimService.pipe(
|
|
176
|
+
Effect.flatMap((service) =>
|
|
177
|
+
service.inspect("example", undefined, root),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
await runTestEffect(
|
|
182
|
+
ClaimService.pipe(
|
|
183
|
+
Effect.flatMap((service) =>
|
|
184
|
+
service.claim(
|
|
185
|
+
{
|
|
186
|
+
taskId: "example",
|
|
187
|
+
claimant: "orchestrator",
|
|
188
|
+
runner: "agent",
|
|
189
|
+
sessionId: "session-1",
|
|
190
|
+
revision: inspected.revision,
|
|
191
|
+
},
|
|
192
|
+
root,
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
await expect(
|
|
198
|
+
runTestEffect(
|
|
199
|
+
VcsMigrationService.pipe(
|
|
200
|
+
Effect.flatMap((service) =>
|
|
201
|
+
service.migrate("jj", root, { apply: true }),
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
).rejects.toThrow("is active")
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("blocks dirty workspaces", async () => {
|
|
209
|
+
if (!Bun.which("jj")) return
|
|
210
|
+
await Bun.write(join(root, "tasks/example/code/agency/DIRTY.md"), "dirty\n")
|
|
211
|
+
await expect(
|
|
212
|
+
runTestEffect(
|
|
213
|
+
VcsMigrationService.pipe(
|
|
214
|
+
Effect.flatMap((service) =>
|
|
215
|
+
service.migrate("jj", root, { apply: true }),
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
).rejects.toThrow("must be clean")
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test("blocks jj-only heads when migrating to Git", async () => {
|
|
223
|
+
if (!Bun.which("jj")) return
|
|
224
|
+
await runTestEffect(
|
|
225
|
+
VcsMigrationService.pipe(
|
|
226
|
+
Effect.flatMap((service) =>
|
|
227
|
+
service.migrate("jj", root, { apply: true }),
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
const repository = join(root, "repos/agency")
|
|
232
|
+
await run(["jj", "-R", repository, "new", "main", "-m", "hidden head"])
|
|
233
|
+
await run(["jj", "-R", repository, "new", "main"])
|
|
234
|
+
|
|
235
|
+
await expect(
|
|
236
|
+
runTestEffect(
|
|
237
|
+
VcsMigrationService.pipe(
|
|
238
|
+
Effect.flatMap((service) =>
|
|
239
|
+
service.migrate("git", root, { apply: true }),
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
).rejects.toThrow("jj-only heads")
|
|
244
|
+
})
|
|
245
|
+
})
|