@markjaquith/agency 2.11.0 → 2.13.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 +14 -4
- package/cli.ts +6 -3
- package/index.ts +1 -0
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +6 -1
- package/src/cli-parser.test.ts +16 -0
- package/src/cli-parser.ts +29 -4
- package/src/cli.test.ts +70 -0
- package/src/commands/work.test.ts +39 -1
- package/src/commands/work.ts +60 -0
- package/src/graph-schema.test.ts +7 -0
- package/src/graph-schema.ts +2 -0
- package/src/readiness.test.ts +47 -0
- package/src/readiness.ts +58 -0
- package/src/services/ContextService.ts +6 -32
- package/src/services/GraphService.test.ts +55 -1
- package/src/services/GraphService.ts +14 -40
- package/src/services/PhaseService.ts +6 -0
- package/src/services/TaskPhaseService.test.ts +43 -0
- package/src/services/TaskService.ts +6 -0
- package/src/services/WorktreeService.test.ts +82 -7
- package/src/services/WorktreeService.ts +199 -41
- package/src/utils/command.ts +1 -0
|
@@ -16,6 +16,22 @@ class WorktreeError extends Data.TaggedError("WorktreeError")<{
|
|
|
16
16
|
readonly message: string
|
|
17
17
|
}> {}
|
|
18
18
|
|
|
19
|
+
interface WorkspaceOperation {
|
|
20
|
+
readonly action: "fetch" | "create-branch" | "create-worktree"
|
|
21
|
+
readonly repo: string
|
|
22
|
+
readonly command: readonly string[]
|
|
23
|
+
readonly status: "planned" | "completed"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WorkspaceCheckout {
|
|
27
|
+
readonly repo: string
|
|
28
|
+
readonly kind: "writable" | "reference"
|
|
29
|
+
readonly path: string
|
|
30
|
+
readonly requestedRef: string
|
|
31
|
+
readonly resolvedCommit: string | null
|
|
32
|
+
readonly action: "created" | "reused"
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
interface ExecutionWorkspace {
|
|
20
36
|
readonly root: string
|
|
21
37
|
readonly taskPath: string
|
|
@@ -24,6 +40,9 @@ interface ExecutionWorkspace {
|
|
|
24
40
|
readonly writablePath: string
|
|
25
41
|
readonly repo: string
|
|
26
42
|
readonly repos: readonly RepositoryReference[]
|
|
43
|
+
readonly dryRun: boolean
|
|
44
|
+
readonly checkouts: readonly WorkspaceCheckout[]
|
|
45
|
+
readonly operations: readonly WorkspaceOperation[]
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
interface GitWorktree {
|
|
@@ -121,7 +140,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
121
140
|
codePath = join(dirname(task.path), "code")
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
yield* fs.createDirectory(codePath)
|
|
143
|
+
if (!options.dryRun) yield* fs.createDirectory(codePath)
|
|
144
|
+
const operations: WorkspaceOperation[] = []
|
|
145
|
+
const checkoutReports: WorkspaceCheckout[] = []
|
|
125
146
|
const checkouts: readonly (
|
|
126
147
|
| { readonly repo: string; readonly branch: string }
|
|
127
148
|
| RepositoryReference
|
|
@@ -146,23 +167,38 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
146
167
|
{ captureOutput: true },
|
|
147
168
|
)
|
|
148
169
|
if (remote.exitCode !== 0) return false
|
|
170
|
+
const command = [
|
|
171
|
+
"git",
|
|
172
|
+
"-C",
|
|
173
|
+
repositoryPath,
|
|
174
|
+
"fetch",
|
|
175
|
+
"origin",
|
|
176
|
+
...(ref ? [ref] : []),
|
|
177
|
+
]
|
|
178
|
+
if (options.dryRun) {
|
|
179
|
+
operations.push({
|
|
180
|
+
action: "fetch",
|
|
181
|
+
repo: alias,
|
|
182
|
+
command,
|
|
183
|
+
status: "planned",
|
|
184
|
+
})
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
149
187
|
|
|
150
|
-
const fetch = yield* fs.runCommand(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"-C",
|
|
154
|
-
repositoryPath,
|
|
155
|
-
"fetch",
|
|
156
|
-
"origin",
|
|
157
|
-
...(ref ? [ref] : []),
|
|
158
|
-
],
|
|
159
|
-
{ captureOutput: true },
|
|
160
|
-
)
|
|
188
|
+
const fetch = yield* fs.runCommand(command, {
|
|
189
|
+
captureOutput: true,
|
|
190
|
+
})
|
|
161
191
|
if (fetch.exitCode !== 0) {
|
|
162
192
|
return yield* new WorktreeError({
|
|
163
193
|
message: `Failed to fetch '${alias}': ${fetch.stderr}`,
|
|
164
194
|
})
|
|
165
195
|
}
|
|
196
|
+
operations.push({
|
|
197
|
+
action: "fetch",
|
|
198
|
+
repo: alias,
|
|
199
|
+
command,
|
|
200
|
+
status: "completed",
|
|
201
|
+
})
|
|
166
202
|
return true
|
|
167
203
|
})
|
|
168
204
|
|
|
@@ -183,7 +219,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
183
219
|
message: `Failed to inspect worktrees for '${alias}': ${listed.stderr}`,
|
|
184
220
|
})
|
|
185
221
|
}
|
|
186
|
-
const canonicalCodePath = yield* fs.
|
|
222
|
+
const canonicalCodePath = (yield* fs.exists(codePath))
|
|
223
|
+
? yield* fs.realPath(codePath)
|
|
224
|
+
: resolve(codePath)
|
|
187
225
|
const canonicalCheckoutPath = join(canonicalCodePath, alias)
|
|
188
226
|
const worktrees: GitWorktree[] = []
|
|
189
227
|
for (const worktree of parseWorktreeList(listed.stdout)) {
|
|
@@ -212,7 +250,17 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
212
250
|
})
|
|
213
251
|
}
|
|
214
252
|
if (yield* fs.isDirectory(checkoutPath)) {
|
|
215
|
-
if (registeredAtPath?.branch === branchRef)
|
|
253
|
+
if (registeredAtPath?.branch === branchRef) {
|
|
254
|
+
checkoutReports.push({
|
|
255
|
+
repo: alias,
|
|
256
|
+
kind: "writable",
|
|
257
|
+
path: checkoutPath,
|
|
258
|
+
requestedRef: checkout.branch,
|
|
259
|
+
resolvedCommit: registeredAtPath.head ?? null,
|
|
260
|
+
action: "reused",
|
|
261
|
+
})
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
216
264
|
return yield* new WorktreeError({
|
|
217
265
|
message: `Existing checkout ${checkoutPath} is not registered to branch '${checkout.branch}'`,
|
|
218
266
|
})
|
|
@@ -260,21 +308,29 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
260
308
|
{ captureOutput: true },
|
|
261
309
|
)
|
|
262
310
|
if (branchExists.exitCode !== 0) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
311
|
+
const command = [
|
|
312
|
+
"git",
|
|
313
|
+
"-C",
|
|
314
|
+
repositoryPath,
|
|
315
|
+
"branch",
|
|
316
|
+
checkout.branch,
|
|
317
|
+
execution.base,
|
|
318
|
+
]
|
|
319
|
+
operations.push({
|
|
320
|
+
action: "create-branch",
|
|
321
|
+
repo: alias,
|
|
322
|
+
command,
|
|
323
|
+
status: options.dryRun ? "planned" : "completed",
|
|
324
|
+
})
|
|
325
|
+
if (!options.dryRun) {
|
|
326
|
+
const createBranch = yield* fs.runCommand(command, {
|
|
327
|
+
captureOutput: true,
|
|
277
328
|
})
|
|
329
|
+
if (createBranch.exitCode !== 0) {
|
|
330
|
+
return yield* new WorktreeError({
|
|
331
|
+
message: `Failed to create branch '${checkout.branch}': ${createBranch.stderr}`,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
278
334
|
}
|
|
279
335
|
}
|
|
280
336
|
args = [
|
|
@@ -287,6 +343,48 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
287
343
|
checkout.branch,
|
|
288
344
|
]
|
|
289
345
|
}
|
|
346
|
+
if (options.dryRun) {
|
|
347
|
+
operations.push({
|
|
348
|
+
action: "create-worktree",
|
|
349
|
+
repo: alias,
|
|
350
|
+
command: args,
|
|
351
|
+
status: "planned",
|
|
352
|
+
})
|
|
353
|
+
let resolved = yield* fs.runCommand(
|
|
354
|
+
[
|
|
355
|
+
"git",
|
|
356
|
+
"-C",
|
|
357
|
+
repositoryPath,
|
|
358
|
+
"rev-parse",
|
|
359
|
+
"--verify",
|
|
360
|
+
`${checkout.branch}^{commit}`,
|
|
361
|
+
],
|
|
362
|
+
{ captureOutput: true },
|
|
363
|
+
)
|
|
364
|
+
if (resolved.exitCode !== 0) {
|
|
365
|
+
resolved = yield* fs.runCommand(
|
|
366
|
+
[
|
|
367
|
+
"git",
|
|
368
|
+
"-C",
|
|
369
|
+
repositoryPath,
|
|
370
|
+
"rev-parse",
|
|
371
|
+
"--verify",
|
|
372
|
+
`${execution.base}^{commit}`,
|
|
373
|
+
],
|
|
374
|
+
{ captureOutput: true },
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
checkoutReports.push({
|
|
378
|
+
repo: alias,
|
|
379
|
+
kind: "writable",
|
|
380
|
+
path: checkoutPath,
|
|
381
|
+
requestedRef: checkout.branch,
|
|
382
|
+
resolvedCommit:
|
|
383
|
+
resolved.exitCode === 0 ? resolved.stdout.trim() : null,
|
|
384
|
+
action: "created",
|
|
385
|
+
})
|
|
386
|
+
continue
|
|
387
|
+
}
|
|
290
388
|
|
|
291
389
|
if (config.worktreeCreateCommand) {
|
|
292
390
|
verboseLog(`Running worktree command: ${formatCommand(args)}`)
|
|
@@ -308,6 +406,24 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
308
406
|
message: `Worktree command did not create ${checkoutPath}`,
|
|
309
407
|
})
|
|
310
408
|
}
|
|
409
|
+
operations.push({
|
|
410
|
+
action: "create-worktree",
|
|
411
|
+
repo: alias,
|
|
412
|
+
command: args,
|
|
413
|
+
status: "completed",
|
|
414
|
+
})
|
|
415
|
+
const head = yield* fs.runCommand(
|
|
416
|
+
["git", "-C", checkoutPath, "rev-parse", "HEAD"],
|
|
417
|
+
{ captureOutput: true },
|
|
418
|
+
)
|
|
419
|
+
checkoutReports.push({
|
|
420
|
+
repo: alias,
|
|
421
|
+
kind: "writable",
|
|
422
|
+
path: checkoutPath,
|
|
423
|
+
requestedRef: checkout.branch,
|
|
424
|
+
resolvedCommit: head.exitCode === 0 ? head.stdout.trim() : null,
|
|
425
|
+
action: "created",
|
|
426
|
+
})
|
|
311
427
|
} else {
|
|
312
428
|
const fetched = isCommitId(checkout.ref)
|
|
313
429
|
? false
|
|
@@ -349,6 +465,14 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
349
465
|
currentHead.exitCode === 0 &&
|
|
350
466
|
currentHead.stdout.trim() === commit
|
|
351
467
|
) {
|
|
468
|
+
checkoutReports.push({
|
|
469
|
+
repo: alias,
|
|
470
|
+
kind: "reference",
|
|
471
|
+
path: checkoutPath,
|
|
472
|
+
requestedRef: checkout.ref,
|
|
473
|
+
resolvedCommit: commit,
|
|
474
|
+
action: "reused",
|
|
475
|
+
})
|
|
352
476
|
continue
|
|
353
477
|
}
|
|
354
478
|
return yield* new WorktreeError({
|
|
@@ -360,24 +484,55 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
360
484
|
message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
|
|
361
485
|
})
|
|
362
486
|
}
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
|
|
487
|
+
const command = [
|
|
488
|
+
"git",
|
|
489
|
+
"-C",
|
|
490
|
+
repositoryPath,
|
|
491
|
+
"worktree",
|
|
492
|
+
"add",
|
|
493
|
+
"--detach",
|
|
494
|
+
checkoutPath,
|
|
495
|
+
commit,
|
|
496
|
+
]
|
|
497
|
+
if (options.dryRun) {
|
|
498
|
+
operations.push({
|
|
499
|
+
action: "create-worktree",
|
|
500
|
+
repo: alias,
|
|
501
|
+
command,
|
|
502
|
+
status: "planned",
|
|
503
|
+
})
|
|
504
|
+
checkoutReports.push({
|
|
505
|
+
repo: alias,
|
|
506
|
+
kind: "reference",
|
|
507
|
+
path: checkoutPath,
|
|
508
|
+
requestedRef: checkout.ref,
|
|
509
|
+
resolvedCommit: commit,
|
|
510
|
+
action: "created",
|
|
511
|
+
})
|
|
512
|
+
continue
|
|
513
|
+
}
|
|
514
|
+
const result = yield* fs.runCommand(command, {
|
|
515
|
+
captureOutput: true,
|
|
516
|
+
})
|
|
376
517
|
if (result.exitCode !== 0) {
|
|
377
518
|
return yield* new WorktreeError({
|
|
378
519
|
message: `Failed to create worktree for '${alias}': ${result.stderr}`,
|
|
379
520
|
})
|
|
380
521
|
}
|
|
522
|
+
operations.push({
|
|
523
|
+
action: "create-worktree",
|
|
524
|
+
repo: alias,
|
|
525
|
+
command,
|
|
526
|
+
status: "completed",
|
|
527
|
+
})
|
|
528
|
+
checkoutReports.push({
|
|
529
|
+
repo: alias,
|
|
530
|
+
kind: "reference",
|
|
531
|
+
path: checkoutPath,
|
|
532
|
+
requestedRef: checkout.ref,
|
|
533
|
+
resolvedCommit: commit,
|
|
534
|
+
action: "created",
|
|
535
|
+
})
|
|
381
536
|
}
|
|
382
537
|
}
|
|
383
538
|
|
|
@@ -389,6 +544,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
389
544
|
writablePath: join(codePath, execution.repo),
|
|
390
545
|
repo: execution.repo,
|
|
391
546
|
repos: execution.repos ?? [],
|
|
547
|
+
dryRun: options.dryRun === true,
|
|
548
|
+
checkouts: checkoutReports,
|
|
549
|
+
operations,
|
|
392
550
|
} satisfies ExecutionWorkspace
|
|
393
551
|
}),
|
|
394
552
|
|
package/src/utils/command.ts
CHANGED