@markjaquith/agency 2.52.0 → 2.52.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/cli-main.ts +854 -0
- package/cli.ts +15 -850
- package/package.json +2 -1
- package/src/services/GraphMutationService.test.ts +71 -0
- package/src/services/GraphMutationService.ts +14 -2
- package/src/services/TaskService.ts +1 -1
- package/src/services/VcsMigrationService.test.ts +46 -0
- package/src/services/VcsMigrationService.ts +35 -21
- package/src/services/VersionControlService.ts +9 -3
- package/src/services/WorktreeService.ts +234 -40
- package/src/vcs-status-fast.ts +315 -0
- package/src/workbase/opencode-plugin-file.ts +31 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.52.
|
|
3
|
+
"version": "2.52.2",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"index.ts",
|
|
22
22
|
"cli.ts",
|
|
23
|
+
"cli-main.ts",
|
|
23
24
|
"src",
|
|
24
25
|
"schemas",
|
|
25
26
|
"fixtures/protocol",
|
|
@@ -158,6 +158,77 @@ describe("GraphMutationService", () => {
|
|
|
158
158
|
)
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
+
test("sets and clears pull request metadata with materialized code", async () => {
|
|
162
|
+
await mkdir(join(root, "tasks/alpha/code/agency"), { recursive: true })
|
|
163
|
+
await mkdir(join(root, "tasks/multi/phases/build/code/agency"), {
|
|
164
|
+
recursive: true,
|
|
165
|
+
})
|
|
166
|
+
const taskPr = "https://github.com/example/agency/pull/12"
|
|
167
|
+
const phasePr = "https://github.com/example/agency/pull/13"
|
|
168
|
+
|
|
169
|
+
await runTestEffect(
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
const mutations = yield* GraphMutationService
|
|
172
|
+
yield* mutations.updateTask("alpha", { pr: taskPr }, root)
|
|
173
|
+
yield* mutations.updatePhase("multi", "build", { pr: phasePr }, root)
|
|
174
|
+
const tasks = yield* TaskService
|
|
175
|
+
const phases = yield* PhaseService
|
|
176
|
+
expect((yield* tasks.show("alpha", root)).data).toMatchObject({
|
|
177
|
+
pr: taskPr,
|
|
178
|
+
})
|
|
179
|
+
expect((yield* phases.show("multi", "build", root)).data.pr).toBe(
|
|
180
|
+
phasePr,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
yield* mutations.updateTask("alpha", { pr: null }, root)
|
|
184
|
+
yield* mutations.updatePhase("multi", "build", { pr: null }, root)
|
|
185
|
+
expect((yield* tasks.show("alpha", root)).data).toMatchObject({
|
|
186
|
+
pr: null,
|
|
187
|
+
})
|
|
188
|
+
expect((yield* phases.show("multi", "build", root)).data.pr).toBeNull()
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test("rejects checkout topology updates with materialized code", async () => {
|
|
194
|
+
await mkdir(join(root, "tasks/alpha/code/agency"), { recursive: true })
|
|
195
|
+
await mkdir(join(root, "tasks/multi/phases/build/code/agency"), {
|
|
196
|
+
recursive: true,
|
|
197
|
+
})
|
|
198
|
+
const topologyUpdates = [
|
|
199
|
+
{ repo: "docs" },
|
|
200
|
+
{ repos: [{ repo: "docs", ref: "main" }] },
|
|
201
|
+
{ branch: "feature/materialized" },
|
|
202
|
+
{ base: "develop" },
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
for (const updates of topologyUpdates) {
|
|
206
|
+
await expect(
|
|
207
|
+
runTestEffect(
|
|
208
|
+
Effect.gen(function* () {
|
|
209
|
+
return yield* (yield* GraphMutationService).updateTask(
|
|
210
|
+
"alpha",
|
|
211
|
+
updates,
|
|
212
|
+
root,
|
|
213
|
+
)
|
|
214
|
+
}),
|
|
215
|
+
),
|
|
216
|
+
).rejects.toThrow("materialized code")
|
|
217
|
+
await expect(
|
|
218
|
+
runTestEffect(
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
return yield* (yield* GraphMutationService).updatePhase(
|
|
221
|
+
"multi",
|
|
222
|
+
"build",
|
|
223
|
+
updates,
|
|
224
|
+
root,
|
|
225
|
+
)
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
).rejects.toThrow("materialized code")
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
161
232
|
test("preserves dependency order and rejects cycles", async () => {
|
|
162
233
|
await runTestEffect(
|
|
163
234
|
Effect.gen(function* () {
|
|
@@ -360,6 +360,12 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
|
|
|
360
360
|
"base",
|
|
361
361
|
"pr",
|
|
362
362
|
].some((key) => updates[key as keyof TaskUpdates] !== undefined)
|
|
363
|
+
const checkoutTopologyChange = [
|
|
364
|
+
"repo",
|
|
365
|
+
"repos",
|
|
366
|
+
"branch",
|
|
367
|
+
"base",
|
|
368
|
+
].some((key) => updates[key as keyof TaskUpdates] !== undefined)
|
|
363
369
|
if ("review" in record.data && executionChange) {
|
|
364
370
|
return yield* new GraphMutationError({
|
|
365
371
|
message: `Review task '${id}' does not have writable execution metadata`,
|
|
@@ -390,7 +396,7 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
|
|
|
390
396
|
})
|
|
391
397
|
}
|
|
392
398
|
if (
|
|
393
|
-
|
|
399
|
+
checkoutTopologyChange &&
|
|
394
400
|
(yield* fs.isDirectory(join(dirname(record.path), "code")))
|
|
395
401
|
) {
|
|
396
402
|
return yield* new GraphMutationError({
|
|
@@ -506,8 +512,14 @@ export class GraphMutationService extends Effect.Service<GraphMutationService>()
|
|
|
506
512
|
"base",
|
|
507
513
|
"pr",
|
|
508
514
|
].some((key) => updates[key as keyof PhaseUpdates] !== undefined)
|
|
515
|
+
const checkoutTopologyChange = [
|
|
516
|
+
"repo",
|
|
517
|
+
"repos",
|
|
518
|
+
"branch",
|
|
519
|
+
"base",
|
|
520
|
+
].some((key) => updates[key as keyof PhaseUpdates] !== undefined)
|
|
509
521
|
if (
|
|
510
|
-
|
|
522
|
+
checkoutTopologyChange &&
|
|
511
523
|
(yield* fs.isDirectory(join(dirname(record.path), "code")))
|
|
512
524
|
) {
|
|
513
525
|
return yield* new GraphMutationError({
|
|
@@ -7,6 +7,7 @@ import { ClaimService } from "./ClaimService"
|
|
|
7
7
|
import { TaskService } from "./TaskService"
|
|
8
8
|
import { VcsMigrationService } from "./VcsMigrationService"
|
|
9
9
|
import { WorktreeService } from "./WorktreeService"
|
|
10
|
+
import { runVcsStatusFast } from "../vcs-status-fast"
|
|
10
11
|
|
|
11
12
|
const run = async (args: string[], cwd?: string) => {
|
|
12
13
|
const child = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" })
|
|
@@ -72,6 +73,51 @@ describe("VcsMigrationService", () => {
|
|
|
72
73
|
|
|
73
74
|
afterEach(async () => cleanupTempDir(root))
|
|
74
75
|
|
|
76
|
+
test("reports same-backend status for materialized workspaces", async () => {
|
|
77
|
+
const status = await runTestEffect(
|
|
78
|
+
VcsMigrationService.pipe(
|
|
79
|
+
Effect.flatMap((service) => service.status(root)),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
expect(status).toMatchObject({
|
|
83
|
+
configured: "git",
|
|
84
|
+
source: "git",
|
|
85
|
+
target: "git",
|
|
86
|
+
workspaceCount: 2,
|
|
87
|
+
blockers: [],
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("fast jj status matches the validated service result", async () => {
|
|
92
|
+
if (!Bun.which("jj")) return
|
|
93
|
+
await runTestEffect(
|
|
94
|
+
VcsMigrationService.pipe(
|
|
95
|
+
Effect.flatMap((service) =>
|
|
96
|
+
service.migrate("jj", root, { apply: true }),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
const taskPath = join(root, "tasks/example/TASK.md")
|
|
101
|
+
const content = await Bun.file(taskPath).text()
|
|
102
|
+
const withoutReference = content.replace(
|
|
103
|
+
/\nrepos:\n(?: .+\n)+(?=branch:)/,
|
|
104
|
+
"\n",
|
|
105
|
+
)
|
|
106
|
+
expect(withoutReference).not.toBe(content)
|
|
107
|
+
await Bun.write(taskPath, withoutReference)
|
|
108
|
+
|
|
109
|
+
const validated = await runTestEffect(
|
|
110
|
+
VcsMigrationService.pipe(
|
|
111
|
+
Effect.flatMap((service) => service.status(root)),
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
const output: string[] = []
|
|
115
|
+
expect(
|
|
116
|
+
await runVcsStatusFast(true, root, (line) => output.push(line)),
|
|
117
|
+
).toBe(true)
|
|
118
|
+
expect(JSON.parse(output.join("\n")).result).toEqual(validated)
|
|
119
|
+
})
|
|
120
|
+
|
|
75
121
|
test("migrates Git worktrees to jj workspaces and back", async () => {
|
|
76
122
|
if (!Bun.which("jj")) return
|
|
77
123
|
const checkout = join(root, "tasks/example/code/agency")
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
runLifecycleTransaction,
|
|
9
9
|
} from "./LifecycleTransaction"
|
|
10
10
|
import { FileSystemService } from "./FileSystemService"
|
|
11
|
-
import { PhaseService } from "./PhaseService"
|
|
11
|
+
import { PhaseService, type PhaseRecord } from "./PhaseService"
|
|
12
12
|
import { RepositoryService } from "./RepositoryService"
|
|
13
13
|
import { TaskService } from "./TaskService"
|
|
14
14
|
import {
|
|
@@ -182,15 +182,19 @@ const executionRecords = (root: string) =>
|
|
|
182
182
|
Effect.gen(function* () {
|
|
183
183
|
const tasks = yield* TaskService
|
|
184
184
|
const phases = yield* PhaseService
|
|
185
|
+
const taskRecords = yield* tasks.list(root)
|
|
186
|
+
const phasesByTask = new Map<string, readonly PhaseRecord[]>()
|
|
185
187
|
const records: {
|
|
186
188
|
taskId: string
|
|
187
189
|
phaseId?: string
|
|
188
190
|
status: WorkStatus
|
|
189
191
|
claimActive: boolean
|
|
190
192
|
}[] = []
|
|
191
|
-
for (const task of
|
|
193
|
+
for (const task of taskRecords) {
|
|
192
194
|
if ("phases" in task.data) {
|
|
193
|
-
|
|
195
|
+
const phaseRecords = yield* phases.list(task.id, root)
|
|
196
|
+
phasesByTask.set(task.id, phaseRecords)
|
|
197
|
+
for (const phase of phaseRecords) {
|
|
194
198
|
records.push({
|
|
195
199
|
taskId: task.id,
|
|
196
200
|
phaseId: phase.id,
|
|
@@ -206,7 +210,7 @@ const executionRecords = (root: string) =>
|
|
|
206
210
|
})
|
|
207
211
|
}
|
|
208
212
|
}
|
|
209
|
-
return records
|
|
213
|
+
return { records, taskRecords, phasesByTask }
|
|
210
214
|
})
|
|
211
215
|
|
|
212
216
|
const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
|
|
@@ -241,7 +245,8 @@ const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
|
|
|
241
245
|
})
|
|
242
246
|
}
|
|
243
247
|
|
|
244
|
-
const
|
|
248
|
+
const execution = yield* executionRecords(root)
|
|
249
|
+
const records = execution.records
|
|
245
250
|
for (const record of records) {
|
|
246
251
|
if (record.claimActive) {
|
|
247
252
|
const label = record.phaseId
|
|
@@ -339,7 +344,15 @@ const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
|
|
|
339
344
|
}
|
|
340
345
|
|
|
341
346
|
const workspacePlans: WorkspacePlan[] = []
|
|
342
|
-
const inspected = yield* Effect.either(
|
|
347
|
+
const inspected = yield* Effect.either(
|
|
348
|
+
worktrees.list(root, {
|
|
349
|
+
materializedOnly: !blockers.some(
|
|
350
|
+
(blocker) => blocker.kind === "repository",
|
|
351
|
+
),
|
|
352
|
+
tasks: execution.taskRecords,
|
|
353
|
+
phasesByTask: execution.phasesByTask,
|
|
354
|
+
}),
|
|
355
|
+
)
|
|
343
356
|
if (Either.isLeft(inspected)) {
|
|
344
357
|
blockers.push({
|
|
345
358
|
kind: "workspace-conflict",
|
|
@@ -377,26 +390,27 @@ const inspectMigration = (startPath: string, requestedTarget?: VcsKind) =>
|
|
|
377
390
|
continue
|
|
378
391
|
}
|
|
379
392
|
const sourceName =
|
|
380
|
-
source === "jj"
|
|
393
|
+
source !== target && source === "jj"
|
|
381
394
|
? ((yield* sourceBackend.listWorkspaces(
|
|
382
395
|
join(root, "repos", checkout.repo),
|
|
383
396
|
)).find((item) => item.path === checkout.registeredPath)
|
|
384
397
|
?.name ?? null)
|
|
385
398
|
: null
|
|
386
|
-
const previousBranchCommit =
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
399
|
+
const previousBranchCommit =
|
|
400
|
+
source !== target && checkout.actualBranch
|
|
401
|
+
? yield* command(
|
|
402
|
+
fs,
|
|
403
|
+
[
|
|
404
|
+
"git",
|
|
405
|
+
"-C",
|
|
406
|
+
join(root, "repos", checkout.repo),
|
|
407
|
+
"rev-parse",
|
|
408
|
+
"--verify",
|
|
409
|
+
`${checkout.actualBranch}^{commit}`,
|
|
410
|
+
],
|
|
411
|
+
`Failed to inspect branch '${checkout.actualBranch}'`,
|
|
412
|
+
).pipe(Effect.catchAll(() => Effect.succeed(null)))
|
|
413
|
+
: null
|
|
400
414
|
workspacePlans.push({
|
|
401
415
|
taskId: inspection.owner.taskId,
|
|
402
416
|
...(inspection.owner.phaseId
|
|
@@ -3,11 +3,13 @@ import { FileSystemService } from "./FileSystemService"
|
|
|
3
3
|
import { WorkbaseService } from "./WorkbaseService"
|
|
4
4
|
import type { VersionControlKind } from "../workbase/version-control"
|
|
5
5
|
|
|
6
|
-
interface RegisteredWorkspace {
|
|
6
|
+
export interface RegisteredWorkspace {
|
|
7
7
|
readonly name: string | null
|
|
8
8
|
readonly path: string
|
|
9
9
|
readonly commit: string | null
|
|
10
10
|
readonly branch: string | null
|
|
11
|
+
readonly head?: string | null
|
|
12
|
+
readonly dirty?: boolean
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface VersionControlBackend {
|
|
@@ -305,7 +307,7 @@ export class JjVersionControlService extends Effect.Service<JjVersionControlServ
|
|
|
305
307
|
"workspace",
|
|
306
308
|
"list",
|
|
307
309
|
"-T",
|
|
308
|
-
'name ++ "\\t" ++ root ++ "\\t" ++ target.commit_id() ++ "\\n"',
|
|
310
|
+
'name ++ "\\t" ++ root ++ "\\t" ++ target.commit_id() ++ "\\t" ++ target.parents().map(|commit| commit.commit_id()).join(",") ++ "\\t" ++ target.empty() ++ "\\n"',
|
|
309
311
|
]),
|
|
310
312
|
{ captureOutput: true },
|
|
311
313
|
),
|
|
@@ -315,12 +317,16 @@ export class JjVersionControlService extends Effect.Service<JjVersionControlServ
|
|
|
315
317
|
.split("\n")
|
|
316
318
|
.filter(Boolean)
|
|
317
319
|
.map((line) => {
|
|
318
|
-
const [name, path, commit] = line.split("\t")
|
|
320
|
+
const [name, path, commit, parents, empty] = line.split("\t")
|
|
321
|
+
const parentCommits = parents?.split(",").filter(Boolean) ?? []
|
|
319
322
|
return {
|
|
320
323
|
name: name || null,
|
|
321
324
|
path: path!,
|
|
322
325
|
commit: commit || null,
|
|
323
326
|
branch: null,
|
|
327
|
+
head:
|
|
328
|
+
parentCommits.length === 1 ? parentCommits[0]! : undefined,
|
|
329
|
+
dirty: empty === "false",
|
|
324
330
|
}
|
|
325
331
|
})
|
|
326
332
|
}),
|