@markjaquith/agency 2.71.0 → 2.71.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.71.0",
3
+ "version": "2.71.2",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -65,6 +65,7 @@
65
65
  "scripts": {
66
66
  "postinstall": "bun scripts/install-pi-extension.ts install",
67
67
  "preuninstall": "bun scripts/install-pi-extension.ts uninstall",
68
+ "benchmark:workbase": "bun scripts/benchmark-workbase.ts",
68
69
  "benchmark:sync": "bun scripts/benchmark-sync.ts",
69
70
  "test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
70
71
  "test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
@@ -394,6 +394,15 @@ describe("work command", () => {
394
394
  target: "execution-unit:task/example",
395
395
  override: true,
396
396
  })
397
+ expect(forced.materializeOptions[0]?.validationAlreadyPerformed).toBe(false)
398
+ })
399
+
400
+ test("reuses successful readiness validation during materialization", async () => {
401
+ const harness = createHarness()
402
+
403
+ await harness.run({ taskId: "example", opencode: true })
404
+
405
+ expect(harness.materializeOptions[0]?.validationAlreadyPerformed).toBe(true)
397
406
  })
398
407
 
399
408
  test("reopens forced terminal tasks through open before launching as working", async () => {
@@ -237,6 +237,7 @@ export const work = (
237
237
  if (!target) return
238
238
  }
239
239
  yield* readiness.guardWorkTarget(targetNodeId(target), root, options.force)
240
+ const validationAlreadyPerformed = !options.force
240
241
 
241
242
  const continuing =
242
243
  target.kind !== "epic" &&
@@ -257,7 +258,10 @@ export const work = (
257
258
  const phaseId = target.kind === "phase" ? target.phaseId : undefined
258
259
  progress.start("Preparing workspace...")
259
260
  const workspace = yield* worktrees
260
- .materialize(taskId, phaseId, root, options)
261
+ .materialize(taskId, phaseId, root, {
262
+ ...options,
263
+ validationAlreadyPerformed,
264
+ })
261
265
  .pipe(
262
266
  Effect.tap(() =>
263
267
  Effect.sync(() => progress.succeed("Workspace ready")),
@@ -87,14 +87,7 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
87
87
 
88
88
  readFile: (path: string) =>
89
89
  Effect.tryPromise({
90
- try: async () => {
91
- const file = Bun.file(path)
92
- const exists = await file.exists()
93
- if (!exists) {
94
- throw new Error(`File not found: ${path}`)
95
- }
96
- return await file.text()
97
- },
90
+ try: () => Bun.file(path).text(),
98
91
  catch: () => new FileNotFoundError({ path }),
99
92
  }),
100
93
 
@@ -144,13 +144,15 @@ export class GraphService extends Effect.Service<GraphService>()(
144
144
  const phases = new Map<string, Document<PhaseData>>()
145
145
 
146
146
  const directories = (path: string) =>
147
- Effect.gen(function* () {
148
- if (!(yield* fs.isDirectory(path))) return []
149
- return (yield* fs.readDirectory(path))
150
- .filter((entry) => entry.isDirectory)
151
- .map((entry) => entry.name)
152
- .sort()
153
- })
147
+ fs.readDirectory(path).pipe(
148
+ Effect.map((entries) =>
149
+ entries
150
+ .filter((entry) => entry.isDirectory)
151
+ .map((entry) => entry.name)
152
+ .sort(),
153
+ ),
154
+ Effect.catchAll(() => Effect.succeed([])),
155
+ )
154
156
 
155
157
  const readDocument = <S extends Schema.Schema.AnyNoContext>(
156
158
  id: string,
@@ -185,9 +187,11 @@ export class GraphService extends Effect.Service<GraphService>()(
185
187
  epicIds.map((id) =>
186
188
  Effect.gen(function* () {
187
189
  const path = join(root, "epics", id, "EPIC.md")
188
- return (yield* fs.exists(path))
189
- ? yield* readDocument(id, path, EpicFrontmatter)
190
- : null
190
+ return yield* readDocument(id, path, EpicFrontmatter).pipe(
191
+ Effect.catchTag("FileNotFoundError", () =>
192
+ Effect.succeed(null),
193
+ ),
194
+ )
191
195
  }),
192
196
  ),
193
197
  { concurrency: "unbounded" },
@@ -201,9 +205,15 @@ export class GraphService extends Effect.Service<GraphService>()(
201
205
  taskIds.map((id) =>
202
206
  Effect.gen(function* () {
203
207
  const path = join(root, "tasks", id, "TASK.md")
204
- const task = (yield* fs.exists(path))
205
- ? yield* readDocument(id, path, TaskFrontmatter)
206
- : null
208
+ const task = yield* readDocument(
209
+ id,
210
+ path,
211
+ TaskFrontmatter,
212
+ ).pipe(
213
+ Effect.catchTag("FileNotFoundError", () =>
214
+ Effect.succeed(null),
215
+ ),
216
+ )
207
217
  const phaseIds = yield* directories(
208
218
  join(root, "tasks", id, "phases"),
209
219
  )
@@ -218,13 +228,15 @@ export class GraphService extends Effect.Service<GraphService>()(
218
228
  phaseId,
219
229
  "PHASE.md",
220
230
  )
221
- return (yield* fs.exists(phasePath))
222
- ? yield* readDocument(
223
- phaseId,
224
- phasePath,
225
- PhaseFrontmatter,
226
- )
227
- : null
231
+ return yield* readDocument(
232
+ phaseId,
233
+ phasePath,
234
+ PhaseFrontmatter,
235
+ ).pipe(
236
+ Effect.catchTag("FileNotFoundError", () =>
237
+ Effect.succeed(null),
238
+ ),
239
+ )
228
240
  }),
229
241
  ),
230
242
  { concurrency: "unbounded" },
@@ -271,11 +271,11 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
271
271
 
272
272
  while (true) {
273
273
  const configPath = join(current, "agency.json")
274
- if (yield* fs.exists(configPath)) {
275
- const content = yield* fs.readFile(configPath)
274
+ const content = yield* Effect.option(fs.readFile(configPath))
275
+ if (content._tag === "Some") {
276
276
  let input: unknown
277
277
  try {
278
- input = JSON.parse(content)
278
+ input = JSON.parse(content.value)
279
279
  } catch (cause) {
280
280
  return yield* new WorkbaseConfigError({
281
281
  path: configPath,
@@ -668,16 +668,15 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
668
668
  }
669
669
 
670
670
  const readDirectories = (path: string) =>
671
- Effect.gen(function* () {
672
- if (!(yield* fs.isDirectory(path))) {
673
- return []
674
- }
675
- const entries = yield* fs.readDirectory(path)
676
- return entries
677
- .filter((entry) => entry.isDirectory)
678
- .map((entry) => entry.name)
679
- .sort()
680
- })
671
+ fs.readDirectory(path).pipe(
672
+ Effect.map((entries) =>
673
+ entries
674
+ .filter((entry) => entry.isDirectory)
675
+ .map((entry) => entry.name)
676
+ .sort(),
677
+ ),
678
+ Effect.catchAll(() => Effect.succeed([])),
679
+ )
681
680
 
682
681
  const aliases = new Set(yield* service.repositoryAliases(root))
683
682
 
@@ -686,14 +685,13 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
686
685
  schema: S,
687
686
  ) =>
688
687
  Effect.gen(function* () {
689
- if (!(yield* fs.exists(path))) {
688
+ const content = yield* Effect.option(fs.readFile(path))
689
+ if (content._tag === "None") {
690
690
  issue(path, "Required document is missing")
691
691
  return null
692
692
  }
693
-
694
- const content = yield* fs.readFile(path)
695
693
  const parsed = yield* Effect.either(
696
- parseFrontmatter(content, path),
694
+ parseFrontmatter(content.value, path),
697
695
  )
698
696
  if (Either.isLeft(parsed)) {
699
697
  issue(path, parsed.left.message)