@markjaquith/agency 2.52.1 → 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/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",
|
|
@@ -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
|
}),
|
|
@@ -13,6 +13,12 @@ import type { BaseCommandOptions } from "../utils/command"
|
|
|
13
13
|
import { createLoggers } from "../utils/effect"
|
|
14
14
|
import { withWorktreeLocks } from "./WorktreeLock"
|
|
15
15
|
import { VersionControlService } from "./VersionControlService"
|
|
16
|
+
import type {
|
|
17
|
+
RegisteredWorkspace,
|
|
18
|
+
VersionControlBackend,
|
|
19
|
+
} from "./VersionControlService"
|
|
20
|
+
import type { TaskRecord } from "./TaskService"
|
|
21
|
+
import type { PhaseRecord } from "./PhaseService"
|
|
16
22
|
|
|
17
23
|
class WorktreeError extends Data.TaggedError("WorktreeError")<{
|
|
18
24
|
readonly message: string
|
|
@@ -113,6 +119,19 @@ interface WorktreeInspection {
|
|
|
113
119
|
readonly conflicts: readonly WorktreeConflict[]
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
interface InspectionContext {
|
|
123
|
+
readonly root: string
|
|
124
|
+
readonly backend: VersionControlBackend
|
|
125
|
+
readonly tasks: readonly TaskRecord[]
|
|
126
|
+
readonly phasesByTask: ReadonlyMap<string, readonly PhaseRecord[]>
|
|
127
|
+
readonly ownership: ReadonlyMap<string, readonly WorktreeOwner[]>
|
|
128
|
+
readonly workspacesByRepository: ReadonlyMap<
|
|
129
|
+
string,
|
|
130
|
+
readonly RegisteredWorkspace[]
|
|
131
|
+
>
|
|
132
|
+
readonly skipWritableRevisionResolution: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
interface WorktreeLifecycleResult {
|
|
117
136
|
readonly operation: "remove" | "rebuild" | "repair"
|
|
118
137
|
readonly dryRun: boolean
|
|
@@ -176,10 +195,17 @@ interface LifecycleOptions extends BaseCommandOptions {
|
|
|
176
195
|
readonly lockHeld?: boolean
|
|
177
196
|
}
|
|
178
197
|
|
|
198
|
+
interface ListOptions {
|
|
199
|
+
readonly materializedOnly?: boolean
|
|
200
|
+
readonly tasks?: readonly TaskRecord[]
|
|
201
|
+
readonly phasesByTask?: ReadonlyMap<string, readonly PhaseRecord[]>
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
const inspectExecution = (
|
|
180
205
|
taskId: string,
|
|
181
206
|
phaseId: string | undefined,
|
|
182
207
|
startPath: string,
|
|
208
|
+
context?: InspectionContext,
|
|
183
209
|
) =>
|
|
184
210
|
Effect.gen(function* () {
|
|
185
211
|
const fs = yield* FileSystemService
|
|
@@ -187,9 +213,17 @@ const inspectExecution = (
|
|
|
187
213
|
const versionControl = yield* VersionControlService
|
|
188
214
|
const tasks = yield* TaskService
|
|
189
215
|
const phases = yield* PhaseService
|
|
190
|
-
const root = yield* workbase.discover(startPath)
|
|
191
|
-
const backend =
|
|
192
|
-
|
|
216
|
+
const root = context?.root ?? (yield* workbase.discover(startPath))
|
|
217
|
+
const backend =
|
|
218
|
+
context?.backend ?? (yield* versionControl.forWorkbase(root))
|
|
219
|
+
const task = context
|
|
220
|
+
? context.tasks.find((record) => record.id === taskId)
|
|
221
|
+
: yield* tasks.show(taskId, root)
|
|
222
|
+
if (!task) {
|
|
223
|
+
return yield* new WorktreeError({
|
|
224
|
+
message: `Task '${taskId}' does not exist`,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
193
227
|
|
|
194
228
|
let execution:
|
|
195
229
|
| {
|
|
@@ -207,7 +241,16 @@ const inspectExecution = (
|
|
|
207
241
|
message: `Task '${taskId}' has multiple phases; phase ID is required`,
|
|
208
242
|
})
|
|
209
243
|
}
|
|
210
|
-
const phase =
|
|
244
|
+
const phase = context
|
|
245
|
+
? context.phasesByTask
|
|
246
|
+
.get(taskId)
|
|
247
|
+
?.find((record) => record.id === phaseId)
|
|
248
|
+
: yield* phases.show(taskId, phaseId, root)
|
|
249
|
+
if (!phase) {
|
|
250
|
+
return yield* new WorktreeError({
|
|
251
|
+
message: `Phase '${phaseId}' does not exist on task '${taskId}'`,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
211
254
|
execution = phase.data
|
|
212
255
|
owner = {
|
|
213
256
|
kind: "phase",
|
|
@@ -227,31 +270,37 @@ const inspectExecution = (
|
|
|
227
270
|
codePath = join(dirname(task.path), "code")
|
|
228
271
|
}
|
|
229
272
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
273
|
+
let ownership: ReadonlyMap<string, readonly WorktreeOwner[]>
|
|
274
|
+
if (context) {
|
|
275
|
+
ownership = context.ownership
|
|
276
|
+
} else {
|
|
277
|
+
const discoveredOwnership = new Map<string, WorktreeOwner[]>()
|
|
278
|
+
for (const taskRecord of yield* tasks.list(root)) {
|
|
279
|
+
if ("phases" in taskRecord.data) {
|
|
280
|
+
for (const phaseRecord of yield* phases.list(taskRecord.id, root)) {
|
|
281
|
+
const key = `${phaseRecord.data.repo}:${phaseRecord.data.branch}`
|
|
282
|
+
const owners = discoveredOwnership.get(key) ?? []
|
|
283
|
+
owners.push({
|
|
284
|
+
kind: "phase",
|
|
285
|
+
taskId: taskRecord.id,
|
|
286
|
+
phaseId: phaseRecord.id,
|
|
287
|
+
documentPath: phaseRecord.path,
|
|
288
|
+
})
|
|
289
|
+
discoveredOwnership.set(key, owners)
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
if (!("repo" in taskRecord.data)) continue
|
|
293
|
+
const key = `${taskRecord.data.repo}:${taskRecord.data.branch}`
|
|
294
|
+
const owners = discoveredOwnership.get(key) ?? []
|
|
236
295
|
owners.push({
|
|
237
|
-
kind: "
|
|
296
|
+
kind: "task",
|
|
238
297
|
taskId: taskRecord.id,
|
|
239
|
-
|
|
240
|
-
documentPath: phaseRecord.path,
|
|
298
|
+
documentPath: taskRecord.path,
|
|
241
299
|
})
|
|
242
|
-
|
|
300
|
+
discoveredOwnership.set(key, owners)
|
|
243
301
|
}
|
|
244
|
-
} else {
|
|
245
|
-
if (!("repo" in taskRecord.data)) continue
|
|
246
|
-
const key = `${taskRecord.data.repo}:${taskRecord.data.branch}`
|
|
247
|
-
const owners = ownership.get(key) ?? []
|
|
248
|
-
owners.push({
|
|
249
|
-
kind: "task",
|
|
250
|
-
taskId: taskRecord.id,
|
|
251
|
-
documentPath: taskRecord.path,
|
|
252
|
-
})
|
|
253
|
-
ownership.set(key, owners)
|
|
254
302
|
}
|
|
303
|
+
ownership = discoveredOwnership
|
|
255
304
|
}
|
|
256
305
|
|
|
257
306
|
const declared: readonly (
|
|
@@ -318,19 +367,27 @@ const inspectExecution = (
|
|
|
318
367
|
const expectedPath = exists
|
|
319
368
|
? yield* fs.realPath(checkoutPath)
|
|
320
369
|
: resolve(checkoutPath)
|
|
321
|
-
const registered =
|
|
370
|
+
const registered =
|
|
371
|
+
context?.workspacesByRepository.get(repositoryPath) ??
|
|
372
|
+
(yield* backend.listWorkspaces(repositoryPath))
|
|
322
373
|
const atPath = registered.find(
|
|
323
374
|
(workspace) => workspace.path === expectedPath,
|
|
324
375
|
)
|
|
325
376
|
const actualCommit = atPath
|
|
326
|
-
?
|
|
377
|
+
? atPath.head !== undefined
|
|
378
|
+
? atPath.head
|
|
379
|
+
: yield* backend.workspaceHead(checkoutPath)
|
|
327
380
|
: null
|
|
328
381
|
const dirty =
|
|
329
|
-
exists && atPath
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
382
|
+
exists && atPath
|
|
383
|
+
? atPath.dirty !== undefined
|
|
384
|
+
? atPath.dirty
|
|
385
|
+
: yield* backend.workspaceDirty(checkoutPath)
|
|
386
|
+
: null
|
|
387
|
+
const expectedCommit =
|
|
388
|
+
"branch" in checkout && context?.skipWritableRevisionResolution
|
|
389
|
+
? actualCommit
|
|
390
|
+
: yield* backend.resolveRevision(repositoryPath, requestedRef)
|
|
334
391
|
|
|
335
392
|
if (owners.length > 1) {
|
|
336
393
|
conflict(
|
|
@@ -647,6 +704,95 @@ const jjWorkspaceName = (
|
|
|
647
704
|
repo: string,
|
|
648
705
|
) => `agency-${taskId}-${phaseId ?? "task"}-${repo}`
|
|
649
706
|
|
|
707
|
+
const buildInspectionContext = (
|
|
708
|
+
startPath: string,
|
|
709
|
+
skipWritableRevisionResolution: boolean,
|
|
710
|
+
preloadedTasks?: readonly TaskRecord[],
|
|
711
|
+
preloadedPhasesByTask?: ReadonlyMap<string, readonly PhaseRecord[]>,
|
|
712
|
+
) =>
|
|
713
|
+
Effect.gen(function* () {
|
|
714
|
+
const fs = yield* FileSystemService
|
|
715
|
+
const workbase = yield* WorkbaseService
|
|
716
|
+
const versionControl = yield* VersionControlService
|
|
717
|
+
const taskService = yield* TaskService
|
|
718
|
+
const phaseService = yield* PhaseService
|
|
719
|
+
const root = yield* workbase.discover(startPath)
|
|
720
|
+
const backend = yield* versionControl.forWorkbase(root)
|
|
721
|
+
const tasks = preloadedTasks ?? (yield* taskService.list(root))
|
|
722
|
+
const phasesByTask = new Map<string, readonly PhaseRecord[]>()
|
|
723
|
+
const ownership = new Map<string, WorktreeOwner[]>()
|
|
724
|
+
const repositoryAliases = new Set<string>()
|
|
725
|
+
|
|
726
|
+
const addExecution = (
|
|
727
|
+
execution:
|
|
728
|
+
| {
|
|
729
|
+
repo: string
|
|
730
|
+
repos?: readonly RepositoryReference[]
|
|
731
|
+
branch: string
|
|
732
|
+
}
|
|
733
|
+
| { review: { repo: string } },
|
|
734
|
+
owner: WorktreeOwner,
|
|
735
|
+
) => {
|
|
736
|
+
if ("review" in execution) {
|
|
737
|
+
repositoryAliases.add(execution.review.repo)
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
repositoryAliases.add(execution.repo)
|
|
741
|
+
for (const reference of execution.repos ?? [])
|
|
742
|
+
repositoryAliases.add(reference.repo)
|
|
743
|
+
const key = `${execution.repo}:${execution.branch}`
|
|
744
|
+
const owners = ownership.get(key) ?? []
|
|
745
|
+
owners.push(owner)
|
|
746
|
+
ownership.set(key, owners)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
for (const task of tasks) {
|
|
750
|
+
if ("phases" in task.data) {
|
|
751
|
+
const phaseRecords =
|
|
752
|
+
preloadedPhasesByTask?.get(task.id) ??
|
|
753
|
+
(yield* phaseService.list(task.id, root))
|
|
754
|
+
phasesByTask.set(task.id, phaseRecords)
|
|
755
|
+
for (const phase of phaseRecords) {
|
|
756
|
+
addExecution(phase.data, {
|
|
757
|
+
kind: "phase",
|
|
758
|
+
taskId: task.id,
|
|
759
|
+
phaseId: phase.id,
|
|
760
|
+
documentPath: phase.path,
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
addExecution(task.data, {
|
|
765
|
+
kind: "task",
|
|
766
|
+
taskId: task.id,
|
|
767
|
+
documentPath: task.path,
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const workspacesByRepository = new Map<
|
|
773
|
+
string,
|
|
774
|
+
readonly RegisteredWorkspace[]
|
|
775
|
+
>()
|
|
776
|
+
for (const alias of repositoryAliases) {
|
|
777
|
+
const repositoryPath = join(root, "repos", alias)
|
|
778
|
+
if (!(yield* fs.exists(repositoryPath))) continue
|
|
779
|
+
workspacesByRepository.set(
|
|
780
|
+
repositoryPath,
|
|
781
|
+
yield* backend.listWorkspaces(repositoryPath),
|
|
782
|
+
)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return {
|
|
786
|
+
root,
|
|
787
|
+
backend,
|
|
788
|
+
tasks,
|
|
789
|
+
phasesByTask,
|
|
790
|
+
ownership,
|
|
791
|
+
workspacesByRepository,
|
|
792
|
+
skipWritableRevisionResolution,
|
|
793
|
+
} satisfies InspectionContext
|
|
794
|
+
})
|
|
795
|
+
|
|
650
796
|
const materializeJj = (options: {
|
|
651
797
|
readonly root: string
|
|
652
798
|
readonly taskId: string
|
|
@@ -935,23 +1081,71 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
935
1081
|
"WorktreeService",
|
|
936
1082
|
{
|
|
937
1083
|
sync: () => ({
|
|
938
|
-
list: (startPath: string = process.cwd()) =>
|
|
1084
|
+
list: (startPath: string = process.cwd(), options: ListOptions = {}) =>
|
|
939
1085
|
Effect.gen(function* () {
|
|
940
|
-
const
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
1086
|
+
const fs = yield* FileSystemService
|
|
1087
|
+
const context = yield* buildInspectionContext(
|
|
1088
|
+
startPath,
|
|
1089
|
+
options.materializedOnly === true,
|
|
1090
|
+
options.tasks,
|
|
1091
|
+
options.phasesByTask,
|
|
1092
|
+
)
|
|
944
1093
|
const inspections: WorktreeInspection[] = []
|
|
945
|
-
|
|
1094
|
+
const shouldInspect = (
|
|
1095
|
+
documentPath: string,
|
|
1096
|
+
execution:
|
|
1097
|
+
| {
|
|
1098
|
+
repo: string
|
|
1099
|
+
repos?: readonly RepositoryReference[]
|
|
1100
|
+
}
|
|
1101
|
+
| { review: { repo: string } },
|
|
1102
|
+
) =>
|
|
1103
|
+
Effect.gen(function* () {
|
|
1104
|
+
if (!options.materializedOnly) return true
|
|
1105
|
+
const codePath = join(dirname(documentPath), "code")
|
|
1106
|
+
const aliases =
|
|
1107
|
+
"review" in execution
|
|
1108
|
+
? [execution.review.repo]
|
|
1109
|
+
: [
|
|
1110
|
+
execution.repo,
|
|
1111
|
+
...(execution.repos ?? []).map(
|
|
1112
|
+
(reference) => reference.repo,
|
|
1113
|
+
),
|
|
1114
|
+
]
|
|
1115
|
+
for (const alias of aliases) {
|
|
1116
|
+
if (yield* fs.isDirectory(join(codePath, alias))) return true
|
|
1117
|
+
}
|
|
1118
|
+
return [...context.workspacesByRepository.values()].some(
|
|
1119
|
+
(workspaces) =>
|
|
1120
|
+
workspaces.some(
|
|
1121
|
+
(workspace) =>
|
|
1122
|
+
workspace.path === codePath ||
|
|
1123
|
+
workspace.path.startsWith(`${codePath}/`),
|
|
1124
|
+
),
|
|
1125
|
+
)
|
|
1126
|
+
})
|
|
1127
|
+
for (const task of context.tasks) {
|
|
946
1128
|
if ("phases" in task.data) {
|
|
947
|
-
for (const phase of
|
|
1129
|
+
for (const phase of context.phasesByTask.get(task.id) ?? []) {
|
|
1130
|
+
if (!(yield* shouldInspect(phase.path, phase.data))) continue
|
|
948
1131
|
inspections.push(
|
|
949
|
-
yield* inspectExecution(
|
|
1132
|
+
yield* inspectExecution(
|
|
1133
|
+
task.id,
|
|
1134
|
+
phase.id,
|
|
1135
|
+
context.root,
|
|
1136
|
+
context,
|
|
1137
|
+
),
|
|
950
1138
|
)
|
|
951
1139
|
}
|
|
952
1140
|
} else {
|
|
1141
|
+
if (!(yield* shouldInspect(task.path, task.data))) continue
|
|
953
1142
|
inspections.push(
|
|
954
|
-
yield* inspectExecution(
|
|
1143
|
+
yield* inspectExecution(
|
|
1144
|
+
task.id,
|
|
1145
|
+
undefined,
|
|
1146
|
+
context.root,
|
|
1147
|
+
context,
|
|
1148
|
+
),
|
|
955
1149
|
)
|
|
956
1150
|
}
|
|
957
1151
|
}
|