@markjaquith/agency 2.71.7 → 2.71.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.71.7",
3
+ "version": "2.71.8",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -71,6 +71,7 @@
71
71
  "benchmark:finish": "bun scripts/benchmark-finish.ts",
72
72
  "benchmark:sync": "bun scripts/benchmark-sync.ts",
73
73
  "benchmark:task": "bun scripts/benchmark-task.ts",
74
+ "benchmark:worktree": "bun scripts/benchmark-worktree.ts",
74
75
  "test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
75
76
  "test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
76
77
  "format": "oxfmt",
@@ -105,7 +105,8 @@ export class PhaseService extends Effect.Service<PhaseService>()(
105
105
  message: `Review task '${taskId}' cannot be converted to phases`,
106
106
  })
107
107
  }
108
- const isMultiPhase = "phases" in task.data
108
+ const taskData = task.data
109
+ const isMultiPhase = "phases" in taskData
109
110
  let firstPhaseId: string | undefined
110
111
  if (!isMultiPhase) {
111
112
  if (!input.firstPhase) {
@@ -129,7 +130,7 @@ export class PhaseService extends Effect.Service<PhaseService>()(
129
130
 
130
131
  if (
131
132
  isMultiPhase &&
132
- task.data.phases.some((phase) => phase.id === id)
133
+ taskData.phases.some((phase) => phase.id === id)
133
134
  ) {
134
135
  return yield* new PhaseError({
135
136
  message: `Phase '${id}' already exists on task '${taskId}'`,
@@ -142,7 +143,7 @@ export class PhaseService extends Effect.Service<PhaseService>()(
142
143
  }
143
144
  const knownPhases = new Set(
144
145
  isMultiPhase
145
- ? task.data.phases.map((phase) => phase.id)
146
+ ? taskData.phases.map((phase) => phase.id)
146
147
  : [firstPhaseId!],
147
148
  )
148
149
  for (const dependency of input.dependsOn ?? []) {
@@ -163,7 +164,7 @@ export class PhaseService extends Effect.Service<PhaseService>()(
163
164
  base: input.base,
164
165
  pr: null,
165
166
  })
166
- const existingExecution = isMultiPhase ? undefined : task.data
167
+ const existingExecution = isMultiPhase ? undefined : taskData
167
168
  const aliases = new Set([
168
169
  data.repo,
169
170
  ...(data.repos ?? []).map((reference) => reference.repo),
@@ -226,15 +227,15 @@ export class PhaseService extends Effect.Service<PhaseService>()(
226
227
  })
227
228
  }
228
229
  const firstData = yield* decodePhase({
229
- repo: task.data.repo,
230
- ...(task.data.repos?.length ? { repos: task.data.repos } : {}),
231
- branch: task.data.branch,
232
- base: task.data.base,
233
- pr: task.data.pr,
234
- status: task.data.status,
235
- ...(task.data.claim ? { claim: task.data.claim } : {}),
236
- ...(task.data.completion
237
- ? { completion: task.data.completion }
230
+ repo: taskData.repo,
231
+ ...(taskData.repos?.length ? { repos: taskData.repos } : {}),
232
+ branch: taskData.branch,
233
+ base: taskData.base,
234
+ pr: taskData.pr,
235
+ status: taskData.status,
236
+ ...(taskData.claim ? { claim: taskData.claim } : {}),
237
+ ...(taskData.completion
238
+ ? { completion: taskData.completion }
238
239
  : {}),
239
240
  })
240
241
  const firstTitle = firstPhaseId!
@@ -514,9 +515,9 @@ export class PhaseService extends Effect.Service<PhaseService>()(
514
515
  }
515
516
 
516
517
  const updatedTaskData = {
517
- ...task.data,
518
+ ...taskData,
518
519
  phases: [
519
- ...task.data.phases,
520
+ ...taskData.phases,
520
521
  {
521
522
  id,
522
523
  ...(input.dependsOn?.length
@@ -552,14 +553,18 @@ export class PhaseService extends Effect.Service<PhaseService>()(
552
553
  } satisfies PhaseRecord
553
554
  }),
554
555
 
555
- list: (taskId: string, startPath: string = process.cwd()) =>
556
+ list: (
557
+ taskId: string,
558
+ startPath: string = process.cwd(),
559
+ parentAlreadyLoaded = false,
560
+ ) =>
556
561
  Effect.gen(function* () {
557
562
  const fs = yield* FileSystemService
558
563
  const workbase = yield* WorkbaseService
559
564
  const tasks = yield* TaskService
560
565
  const root = yield* workbase.discover(startPath)
561
566
  const validTaskId = yield* decodeId(taskId, "task")
562
- yield* tasks.show(validTaskId, root)
567
+ if (!parentAlreadyLoaded) yield* tasks.show(validTaskId, root)
563
568
  const directory = join(root, "tasks", validTaskId, "phases")
564
569
  if (!(yield* fs.isDirectory(directory))) return [] as PhaseRecord[]
565
570
  const entries = (yield* fs.readDirectory(directory))
@@ -586,17 +591,36 @@ export class PhaseService extends Effect.Service<PhaseService>()(
586
591
 
587
592
  show: (taskId: string, id: string, startPath: string = process.cwd()) =>
588
593
  Effect.gen(function* () {
589
- const service = yield* PhaseService
594
+ const fs = yield* FileSystemService
595
+ const workbase = yield* WorkbaseService
596
+ const tasks = yield* TaskService
597
+ const validTaskId = yield* decodeId(taskId, "task")
590
598
  const validId = yield* decodeId(id, "phase")
591
- const record = (yield* service.list(taskId, startPath)).find(
592
- (phase) => phase.id === validId,
599
+ const root = yield* workbase.discover(startPath)
600
+ yield* tasks.show(validTaskId, root)
601
+ const path = join(
602
+ root,
603
+ "tasks",
604
+ validTaskId,
605
+ "phases",
606
+ validId,
607
+ "PHASE.md",
593
608
  )
594
- if (!record) {
609
+ if (!(yield* fs.exists(path))) {
595
610
  return yield* new PhaseError({
596
- message: `Phase '${validId}' does not exist on task '${taskId}'`,
611
+ message: `Phase '${validId}' does not exist on task '${validTaskId}'`,
597
612
  })
598
613
  }
599
- return record
614
+ const content = yield* fs.readFile(path)
615
+ const parsed = yield* parseFrontmatter(content, path)
616
+ return {
617
+ taskId: validTaskId,
618
+ id: validId,
619
+ path,
620
+ content,
621
+ revision: documentRevision(content),
622
+ data: yield* decodePhase(parsed.data),
623
+ } satisfies PhaseRecord
600
624
  }),
601
625
 
602
626
  setStatus: (
@@ -582,17 +582,25 @@ export class TaskService extends Effect.Service<TaskService>()("TaskService", {
582
582
 
583
583
  show: (id: string, startPath: string = process.cwd()) =>
584
584
  Effect.gen(function* () {
585
- const service = yield* TaskService
585
+ const fs = yield* FileSystemService
586
+ const workbase = yield* WorkbaseService
586
587
  const validId = yield* decodeId(id)
587
- const record = (yield* service.list(startPath)).find(
588
- (task) => task.id === validId,
589
- )
590
- if (!record) {
588
+ const root = yield* workbase.discover(startPath)
589
+ const path = join(root, "tasks", validId, "TASK.md")
590
+ if (!(yield* fs.exists(path))) {
591
591
  return yield* new TaskError({
592
592
  message: `Task '${validId}' does not exist`,
593
593
  })
594
594
  }
595
- return record
595
+ const content = yield* fs.readFile(path)
596
+ const parsed = yield* parseFrontmatter(content, path)
597
+ return {
598
+ id: validId,
599
+ path,
600
+ content,
601
+ revision: documentRevision(content),
602
+ data: yield* decodeTask(parsed.data),
603
+ } satisfies TaskRecord
596
604
  }),
597
605
 
598
606
  setStatus: (
@@ -0,0 +1,71 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
+ import { Effect } from "effect"
3
+ import { mkdir } from "node:fs/promises"
4
+ import { join } from "node:path"
5
+ import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
+ import { PhaseService } from "./PhaseService"
7
+ import { TaskService } from "./TaskService"
8
+
9
+ const taskDocument = (id: string) => `---
10
+ ticketUrl: null
11
+ repo: agency
12
+ branch: task/${id}
13
+ base: main
14
+ pr: null
15
+ status: open
16
+ ---
17
+
18
+ # ${id}
19
+ `
20
+
21
+ describe("worktree document loading", () => {
22
+ let root: string
23
+
24
+ beforeEach(async () => {
25
+ root = await createTempDir()
26
+ await Bun.write(join(root, "agency.json"), '{"version":2}\n')
27
+ })
28
+
29
+ afterEach(async () => cleanupTempDir(root))
30
+
31
+ test("shows a task without parsing sibling task documents", async () => {
32
+ await mkdir(join(root, "tasks/target"), { recursive: true })
33
+ await mkdir(join(root, "tasks/broken"), { recursive: true })
34
+ await Bun.write(join(root, "tasks/target/TASK.md"), taskDocument("target"))
35
+ await Bun.write(join(root, "tasks/broken/TASK.md"), "invalid frontmatter")
36
+
37
+ const task = await runTestEffect(
38
+ TaskService.pipe(
39
+ Effect.flatMap((service) => service.show("target", root)),
40
+ ),
41
+ )
42
+
43
+ expect(task.id).toBe("target")
44
+ })
45
+
46
+ test("lists phases from a preloaded task without rereading its parent", async () => {
47
+ const phasePath = join(root, "tasks/multi/phases/build")
48
+ await mkdir(phasePath, { recursive: true })
49
+ await Bun.write(
50
+ join(phasePath, "PHASE.md"),
51
+ `---
52
+ repo: agency
53
+ branch: task/multi-build
54
+ base: main
55
+ pr: null
56
+ status: open
57
+ ---
58
+
59
+ # Build
60
+ `,
61
+ )
62
+
63
+ const phases = await runTestEffect(
64
+ PhaseService.pipe(
65
+ Effect.flatMap((service) => service.list("multi", root, true)),
66
+ ),
67
+ )
68
+
69
+ expect(phases.map(({ id }) => id)).toEqual(["build"])
70
+ })
71
+ })
@@ -1089,7 +1089,7 @@ const buildInspectionContext = (
1089
1089
  if ("phases" in task.data) {
1090
1090
  const phaseRecords =
1091
1091
  preloadedPhasesByTask?.get(task.id) ??
1092
- (yield* phaseService.list(task.id, root))
1092
+ (yield* phaseService.list(task.id, root, true))
1093
1093
  phasesByTask.set(task.id, phaseRecords)
1094
1094
  for (const phase of phaseRecords) {
1095
1095
  addExecution(phase.data, {
@@ -3593,12 +3593,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
3593
3593
  })
3594
3594
  }
3595
3595
 
3596
- const current = yield* inspectExecution(
3597
- taskId,
3598
- phaseId,
3599
- inspection.root,
3600
- )
3601
- if (current.checkouts.some((checkout) => !checkout.exists)) {
3596
+ if (repaired.checkouts.some((checkout) => !checkout.exists)) {
3602
3597
  const workspace = yield* service
3603
3598
  .materialize(taskId, phaseId, inspection.root, {
3604
3599
  ...options,