@markjaquith/agency 3.2.12 → 3.2.13

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": "3.2.12",
3
+ "version": "3.2.13",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -1,12 +1,17 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
2
2
  import { mkdir, readFile as nodeReadFile, rename, rm } from "node:fs/promises"
3
3
  import { dirname, join } from "node:path"
4
+ import { Effect } from "effect"
4
5
  import {
5
6
  captureLogs,
6
7
  cleanupTempDir,
7
8
  createTempDir,
8
9
  runTestEffect,
10
+ trackDocumentReadConcurrency,
9
11
  } from "../test-utils"
12
+ import { documentLoadConcurrency } from "../workbase/document-loading"
13
+ import { ContextService } from "../services/ContextService"
14
+ import { FileSystemService } from "../services/FileSystemService"
10
15
  import { context } from "./context"
11
16
 
12
17
  const write = async (root: string, path: string, content: string) => {
@@ -157,6 +162,33 @@ Phase prose.
157
162
  ])
158
163
  })
159
164
 
165
+ test("bounds document reads across nested task and phase traversal", async () => {
166
+ await Promise.all(
167
+ Array.from({ length: documentLoadConcurrency + 8 }, (_, index) =>
168
+ write(
169
+ root,
170
+ `tasks/agent-contract/phases/extra-${index}/PHASE.md`,
171
+ `---\nrepo: agency\nbranch: extra-${index}\nbase: main\npr: null\nstatus: open\n---\n`,
172
+ ),
173
+ ),
174
+ )
175
+ const fs = await Effect.runPromise(
176
+ FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
177
+ )
178
+ const tracked = trackDocumentReadConcurrency(fs)
179
+
180
+ await runTestEffect(
181
+ ContextService.pipe(
182
+ Effect.flatMap((service) =>
183
+ service.get({ cwd: root, target: "foundations" }),
184
+ ),
185
+ Effect.provideService(FileSystemService, tracked.fs),
186
+ ),
187
+ )
188
+
189
+ expect(tracked.maximum()).toBe(documentLoadConcurrency)
190
+ })
191
+
160
192
  afterEach(async () => {
161
193
  await cleanupTempDir(root)
162
194
  })
@@ -4,7 +4,14 @@ import { Effect } from "effect"
4
4
  import { mkdir } from "node:fs/promises"
5
5
  import { dirname, join } from "node:path"
6
6
  import { AgencyGraph } from "../graph-schema"
7
- import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
7
+ import {
8
+ cleanupTempDir,
9
+ createTempDir,
10
+ runTestEffect,
11
+ trackDocumentReadConcurrency,
12
+ } from "../test-utils"
13
+ import { documentLoadConcurrency } from "../workbase/document-loading"
14
+ import { FileSystemService } from "./FileSystemService"
8
15
  import { GraphService } from "./GraphService"
9
16
  import {
10
17
  VersionControlService,
@@ -209,6 +216,33 @@ describe("GraphService", () => {
209
216
  expect(await getGraph(root)).toEqual(graph)
210
217
  })
211
218
 
219
+ test("bounds document reads across nested task and phase traversal", async () => {
220
+ const root = await createWorkbase()
221
+ roots.push(root)
222
+ await Promise.all(
223
+ Array.from({ length: documentLoadConcurrency + 8 }, (_, index) =>
224
+ write(
225
+ root,
226
+ `tasks/ship/phases/extra-${index}/PHASE.md`,
227
+ `---\nrepo: agency\nbranch: extra-${index}\nbase: main\npr: null\nstatus: open\n---\n`,
228
+ ),
229
+ ),
230
+ )
231
+ const fs = await Effect.runPromise(
232
+ FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
233
+ )
234
+ const tracked = trackDocumentReadConcurrency(fs)
235
+
236
+ await runTestEffect(
237
+ GraphService.pipe(
238
+ Effect.flatMap((service) => service.get({ cwd: root })),
239
+ Effect.provideService(FileSystemService, tracked.fs),
240
+ ),
241
+ )
242
+
243
+ expect(tracked.maximum()).toBe(documentLoadConcurrency)
244
+ })
245
+
212
246
  test("does not resolve a VCS backend when git details are not requested", async () => {
213
247
  const root = await createWorkbase()
214
248
  roots.push(root)
@@ -22,6 +22,7 @@ import {
22
22
  import { parseFrontmatter } from "../workbase/frontmatter"
23
23
  import { normalizePullRequestRecord } from "../workbase/delivery-command"
24
24
  import { documentRevision } from "../workbase/document-revision"
25
+ import { documentLoadConcurrency } from "../workbase/document-loading"
25
26
  import {
26
27
  EpicFrontmatter,
27
28
  PhaseFrontmatter,
@@ -201,7 +202,7 @@ export class GraphService extends Effect.Service<GraphService>()(
201
202
  )
202
203
  }),
203
204
  ),
204
- { concurrency: "unbounded" },
205
+ { concurrency: documentLoadConcurrency },
205
206
  )
206
207
  for (const document of epicDocuments) {
207
208
  if (document) epics.set(document.id, document)
@@ -224,40 +225,37 @@ export class GraphService extends Effect.Service<GraphService>()(
224
225
  const phaseIds = yield* directories(
225
226
  join(root, "tasks", id, "phases"),
226
227
  )
227
- const taskPhases = yield* Effect.all(
228
- phaseIds.map((phaseId) =>
229
- Effect.gen(function* () {
230
- const phasePath = join(
231
- root,
232
- "tasks",
233
- id,
234
- "phases",
235
- phaseId,
236
- "PHASE.md",
237
- )
238
- return yield* readDocument(
239
- phaseId,
240
- phasePath,
241
- PhaseFrontmatter,
242
- ).pipe(
243
- Effect.catchTag("FileNotFoundError", () =>
244
- Effect.succeed(null),
245
- ),
246
- )
247
- }),
228
+ return { id, task, phaseIds }
229
+ }),
230
+ ),
231
+ { concurrency: documentLoadConcurrency },
232
+ )
233
+ const phaseDocuments = yield* Effect.all(
234
+ taskDocuments.flatMap(({ id, phaseIds }) =>
235
+ phaseIds.map((phaseId) => {
236
+ const phasePath = join(
237
+ root,
238
+ "tasks",
239
+ id,
240
+ "phases",
241
+ phaseId,
242
+ "PHASE.md",
243
+ )
244
+ return readDocument(phaseId, phasePath, PhaseFrontmatter).pipe(
245
+ Effect.catchTag("FileNotFoundError", () =>
246
+ Effect.succeed(null),
248
247
  ),
249
- { concurrency: "unbounded" },
248
+ Effect.map((document) => ({ taskId: id, document })),
250
249
  )
251
- return { id, task, phases: taskPhases }
252
250
  }),
253
251
  ),
254
- { concurrency: "unbounded" },
252
+ { concurrency: documentLoadConcurrency },
255
253
  )
256
254
  for (const documents of taskDocuments) {
257
255
  if (documents.task) tasks.set(documents.id, documents.task)
258
- for (const phase of documents.phases) {
259
- if (phase) phases.set(`${documents.id}/${phase.id}`, phase)
260
- }
256
+ }
257
+ for (const { taskId, document } of phaseDocuments) {
258
+ if (document) phases.set(`${taskId}/${document.id}`, document)
261
259
  }
262
260
 
263
261
  const repositoryRecords = new Map<string, RepositoryRecord>()
@@ -26,6 +26,7 @@ import { validateAgents } from "../workbase/agent-command"
26
26
  import { findDependencyCycles } from "../workbase/dependency-graph"
27
27
  import { validateDelivery } from "../workbase/delivery-command"
28
28
  import { documentRevision } from "../workbase/document-revision"
29
+ import { documentLoadConcurrency } from "../workbase/document-loading"
29
30
 
30
31
  class WorkbaseNotFoundError extends Data.TaggedError("WorkbaseNotFoundError")<{
31
32
  readonly message: string
@@ -66,8 +67,6 @@ interface DocumentRecord<T> {
66
67
  readonly data: T
67
68
  }
68
69
 
69
- const validationConcurrency = 32
70
-
71
70
  interface ValidationDocuments {
72
71
  readonly epics: readonly DocumentRecord<EpicData>[]
73
72
  readonly tasks: readonly DocumentRecord<TaskData>[]
@@ -751,7 +750,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
751
750
  return document ? { id, path, ...document } : null
752
751
  }),
753
752
  ),
754
- { concurrency: validationConcurrency },
753
+ { concurrency: documentLoadConcurrency },
755
754
  )
756
755
  for (const document of epicDocuments) {
757
756
  if (document) epics.set(document.id, document)
@@ -793,7 +792,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
793
792
  : null
794
793
  }),
795
794
  ),
796
- { concurrency: validationConcurrency },
795
+ { concurrency: documentLoadConcurrency },
797
796
  )
798
797
  return {
799
798
  id,
@@ -802,7 +801,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
802
801
  }
803
802
  }),
804
803
  ),
805
- { concurrency: validationConcurrency },
804
+ { concurrency: documentLoadConcurrency },
806
805
  )
807
806
  for (const documents of taskDocuments) {
808
807
  if (documents.task) tasks.set(documents.id, documents.task)
package/src/test-utils.ts CHANGED
@@ -31,6 +31,26 @@ export const createTempDir = () => mkdtemp(join(tmpdir(), "agency-test-"))
31
31
  export const cleanupTempDir = (path: string) =>
32
32
  rm(path, { recursive: true, force: true })
33
33
 
34
+ export const trackDocumentReadConcurrency = (fs: FileSystemService) => {
35
+ let active = 0
36
+ let maximum = 0
37
+ return {
38
+ fs: {
39
+ ...fs,
40
+ readFile: (path: string) => {
41
+ if (!/(?:EPIC|TASK|PHASE)\.md$/.test(path)) return fs.readFile(path)
42
+ return Effect.gen(function* () {
43
+ active += 1
44
+ maximum = Math.max(maximum, active)
45
+ yield* Effect.sleep(5)
46
+ return yield* fs.readFile(path)
47
+ }).pipe(Effect.ensuring(Effect.sync(() => (active -= 1))))
48
+ },
49
+ } satisfies FileSystemService,
50
+ maximum: () => maximum,
51
+ }
52
+ }
53
+
34
54
  const TestLayer = Layer.mergeAll(
35
55
  FileSystemService.Default,
36
56
  WorkbaseService.Default,
@@ -0,0 +1 @@
1
+ export const documentLoadConcurrency = 32