@markjaquith/agency 2.71.1 → 2.71.3
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.
|
|
3
|
+
"version": "2.71.3",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"postinstall": "bun scripts/install-pi-extension.ts install",
|
|
67
67
|
"preuninstall": "bun scripts/install-pi-extension.ts uninstall",
|
|
68
68
|
"benchmark:workbase": "bun scripts/benchmark-workbase.ts",
|
|
69
|
+
"benchmark:context": "bun scripts/benchmark-context.ts",
|
|
69
70
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
70
71
|
"test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
|
|
71
72
|
"test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdir, rename, rm } from "node:fs/promises"
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
|
|
2
|
+
import { mkdir, readFile as nodeReadFile, rename, rm } from "node:fs/promises"
|
|
3
3
|
import { dirname, join } from "node:path"
|
|
4
4
|
import {
|
|
5
5
|
captureLogs,
|
|
@@ -343,6 +343,31 @@ status: dropped
|
|
|
343
343
|
})
|
|
344
344
|
})
|
|
345
345
|
|
|
346
|
+
test("reads each workbase document at most once per invocation", async () => {
|
|
347
|
+
const reads = new Map<string, number>()
|
|
348
|
+
const originalFile = Bun.file
|
|
349
|
+
Bun.file = mock((path: string) => {
|
|
350
|
+
const file = originalFile(path)
|
|
351
|
+
return Object.assign(file, {
|
|
352
|
+
text: async () => {
|
|
353
|
+
reads.set(path, (reads.get(path) ?? 0) + 1)
|
|
354
|
+
return nodeReadFile(path, "utf8")
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
}) as unknown as typeof Bun.file
|
|
358
|
+
try {
|
|
359
|
+
await readContext(root, "tasks/agent-contract/phases/context-command")
|
|
360
|
+
} finally {
|
|
361
|
+
Bun.file = originalFile
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const documents = [...reads].filter(([path]) =>
|
|
365
|
+
/(?:EPIC|TASK|PHASE)\.md$/.test(path),
|
|
366
|
+
)
|
|
367
|
+
expect(documents.length).toBeGreaterThan(0)
|
|
368
|
+
expect(documents.every(([, count]) => count === 1)).toBe(true)
|
|
369
|
+
})
|
|
370
|
+
|
|
346
371
|
test("resolves a bare task ID and returns root discovery context", async () => {
|
|
347
372
|
const task = await readContext(root, "agent-contract")
|
|
348
373
|
expect(task.target).toMatchObject({
|
|
@@ -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 () => {
|
package/src/commands/work.ts
CHANGED
|
@@ -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,
|
|
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")),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema, TreeFormatter } from "@effect/schema"
|
|
2
|
-
import { Data, Effect, Either } from "effect"
|
|
2
|
+
import { Data, Deferred, Effect, Either, Exit } from "effect"
|
|
3
3
|
import { join, relative, resolve, sep } from "node:path"
|
|
4
4
|
import { FileSystemService } from "./FileSystemService"
|
|
5
5
|
import { WorkbaseService } from "./WorkbaseService"
|
|
@@ -131,10 +131,76 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
131
131
|
readonly full?: boolean
|
|
132
132
|
}) =>
|
|
133
133
|
Effect.gen(function* () {
|
|
134
|
-
const
|
|
134
|
+
const sourceFs = yield* FileSystemService
|
|
135
135
|
const workbase = yield* WorkbaseService
|
|
136
136
|
const repositoryService = yield* RepositoryService
|
|
137
137
|
const versionControl = yield* VersionControlService
|
|
138
|
+
const memoize = <A, E, R>(
|
|
139
|
+
cache: Map<string, Deferred.Deferred<A, E>>,
|
|
140
|
+
key: string,
|
|
141
|
+
effect: Effect.Effect<A, E, R>,
|
|
142
|
+
) => {
|
|
143
|
+
const cached = cache.get(key)
|
|
144
|
+
if (cached) return Deferred.await(cached)
|
|
145
|
+
return Effect.gen(function* () {
|
|
146
|
+
const deferred = yield* Deferred.make<A, E>()
|
|
147
|
+
cache.set(key, deferred)
|
|
148
|
+
const exit = yield* Effect.exit(effect)
|
|
149
|
+
yield* Deferred.done(deferred, exit)
|
|
150
|
+
return yield* Exit.match(exit, {
|
|
151
|
+
onFailure: Effect.failCause,
|
|
152
|
+
onSuccess: Effect.succeed,
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
const existsCache = new Map<
|
|
157
|
+
string,
|
|
158
|
+
Deferred.Deferred<
|
|
159
|
+
Effect.Effect.Success<ReturnType<typeof sourceFs.exists>>,
|
|
160
|
+
Effect.Effect.Error<ReturnType<typeof sourceFs.exists>>
|
|
161
|
+
>
|
|
162
|
+
>()
|
|
163
|
+
const directoryCache = new Map<
|
|
164
|
+
string,
|
|
165
|
+
Deferred.Deferred<
|
|
166
|
+
Effect.Effect.Success<ReturnType<typeof sourceFs.isDirectory>>,
|
|
167
|
+
Effect.Effect.Error<ReturnType<typeof sourceFs.isDirectory>>
|
|
168
|
+
>
|
|
169
|
+
>()
|
|
170
|
+
const readCache = new Map<
|
|
171
|
+
string,
|
|
172
|
+
Deferred.Deferred<
|
|
173
|
+
Effect.Effect.Success<ReturnType<typeof sourceFs.readFile>>,
|
|
174
|
+
Effect.Effect.Error<ReturnType<typeof sourceFs.readFile>>
|
|
175
|
+
>
|
|
176
|
+
>()
|
|
177
|
+
const listingCache = new Map<
|
|
178
|
+
string,
|
|
179
|
+
Deferred.Deferred<
|
|
180
|
+
Effect.Effect.Success<ReturnType<typeof sourceFs.readDirectory>>,
|
|
181
|
+
Effect.Effect.Error<ReturnType<typeof sourceFs.readDirectory>>
|
|
182
|
+
>
|
|
183
|
+
>()
|
|
184
|
+
const realPathCache = new Map<
|
|
185
|
+
string,
|
|
186
|
+
Deferred.Deferred<
|
|
187
|
+
Effect.Effect.Success<ReturnType<typeof sourceFs.realPath>>,
|
|
188
|
+
Effect.Effect.Error<ReturnType<typeof sourceFs.realPath>>
|
|
189
|
+
>
|
|
190
|
+
>()
|
|
191
|
+
const fs = {
|
|
192
|
+
...sourceFs,
|
|
193
|
+
exists: (path: string) =>
|
|
194
|
+
memoize(existsCache, path, sourceFs.exists(path)),
|
|
195
|
+
isDirectory: (path: string) =>
|
|
196
|
+
memoize(directoryCache, path, sourceFs.isDirectory(path)),
|
|
197
|
+
readFile: (path: string) =>
|
|
198
|
+
memoize(readCache, path, sourceFs.readFile(path)),
|
|
199
|
+
readDirectory: (path: string) =>
|
|
200
|
+
memoize(listingCache, path, sourceFs.readDirectory(path)),
|
|
201
|
+
realPath: (path: string) =>
|
|
202
|
+
memoize(realPathCache, path, sourceFs.realPath(path)),
|
|
203
|
+
} satisfies FileSystemService
|
|
138
204
|
const cwd = resolve(options.cwd ?? process.cwd())
|
|
139
205
|
const suppliedTarget = options.target ?? "."
|
|
140
206
|
const candidate = resolve(cwd, suppliedTarget)
|
|
@@ -142,7 +208,66 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
142
208
|
const { root, config } = yield* workbase.loadConfig(
|
|
143
209
|
candidateExists ? candidate : cwd,
|
|
144
210
|
)
|
|
145
|
-
const
|
|
211
|
+
const sourceBackend = yield* versionControl.forWorkbase(root)
|
|
212
|
+
const revisionCache = new Map<
|
|
213
|
+
string,
|
|
214
|
+
Deferred.Deferred<
|
|
215
|
+
Effect.Effect.Success<
|
|
216
|
+
ReturnType<typeof sourceBackend.resolveRevision>
|
|
217
|
+
>,
|
|
218
|
+
Effect.Effect.Error<
|
|
219
|
+
ReturnType<typeof sourceBackend.resolveRevision>
|
|
220
|
+
>
|
|
221
|
+
>
|
|
222
|
+
>()
|
|
223
|
+
const workspaceCache = new Map<
|
|
224
|
+
string,
|
|
225
|
+
Deferred.Deferred<
|
|
226
|
+
Effect.Effect.Success<
|
|
227
|
+
ReturnType<typeof sourceBackend.listWorkspaces>
|
|
228
|
+
>,
|
|
229
|
+
Effect.Effect.Error<
|
|
230
|
+
ReturnType<typeof sourceBackend.listWorkspaces>
|
|
231
|
+
>
|
|
232
|
+
>
|
|
233
|
+
>()
|
|
234
|
+
const backend = {
|
|
235
|
+
...sourceBackend,
|
|
236
|
+
resolveRevision: (repositoryPath: string, revision: string) =>
|
|
237
|
+
memoize(
|
|
238
|
+
revisionCache,
|
|
239
|
+
`${repositoryPath}\0${revision}`,
|
|
240
|
+
sourceBackend.resolveRevision(repositoryPath, revision),
|
|
241
|
+
),
|
|
242
|
+
listWorkspaces: (repositoryPath: string) =>
|
|
243
|
+
memoize(
|
|
244
|
+
workspaceCache,
|
|
245
|
+
repositoryPath,
|
|
246
|
+
sourceBackend.listWorkspaces(repositoryPath),
|
|
247
|
+
),
|
|
248
|
+
}
|
|
249
|
+
const contextWorkbase = {
|
|
250
|
+
...workbase,
|
|
251
|
+
discover: (startPath?: string) =>
|
|
252
|
+
workbase
|
|
253
|
+
.discover(startPath)
|
|
254
|
+
.pipe(Effect.provideService(FileSystemService, fs)),
|
|
255
|
+
loadConfig: (startPath?: string) =>
|
|
256
|
+
workbase
|
|
257
|
+
.loadConfig(startPath)
|
|
258
|
+
.pipe(Effect.provideService(FileSystemService, fs)),
|
|
259
|
+
repositoryAliases: (startPath?: string) =>
|
|
260
|
+
workbase
|
|
261
|
+
.repositoryAliases(startPath)
|
|
262
|
+
.pipe(Effect.provideService(FileSystemService, fs)),
|
|
263
|
+
}
|
|
264
|
+
const provideContextServices = <A, E, R>(
|
|
265
|
+
effect: Effect.Effect<A, E, R>,
|
|
266
|
+
) =>
|
|
267
|
+
effect.pipe(
|
|
268
|
+
Effect.provideService(FileSystemService, fs),
|
|
269
|
+
Effect.provideService(WorkbaseService, contextWorkbase),
|
|
270
|
+
)
|
|
146
271
|
|
|
147
272
|
if (relative(root, candidate) === "") {
|
|
148
273
|
const compact = !options.full
|
|
@@ -216,7 +341,9 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
216
341
|
}
|
|
217
342
|
}
|
|
218
343
|
|
|
219
|
-
const validation = yield*
|
|
344
|
+
const validation = yield* provideContextServices(
|
|
345
|
+
workbase.validate(root),
|
|
346
|
+
)
|
|
220
347
|
return {
|
|
221
348
|
projection: compact ? "compact" : "complete",
|
|
222
349
|
workbase: {
|
|
@@ -593,7 +720,9 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
593
720
|
})
|
|
594
721
|
}
|
|
595
722
|
|
|
596
|
-
const validation = yield*
|
|
723
|
+
const validation = yield* provideContextServices(
|
|
724
|
+
workbase.validate(root),
|
|
725
|
+
)
|
|
597
726
|
const relevantPaths = new Set<string>(
|
|
598
727
|
[epic?.path, task?.path, phase?.path]
|
|
599
728
|
.filter((path): path is string => Boolean(path))
|
|
@@ -888,10 +1017,9 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
888
1017
|
const codePath = join(entityDirectory, "code")
|
|
889
1018
|
const inspectionWarnings: string[] = []
|
|
890
1019
|
const repositories = new Map(
|
|
891
|
-
(yield* repositoryService.list(root)).map(
|
|
892
|
-
repository.alias,
|
|
893
|
-
|
|
894
|
-
]),
|
|
1020
|
+
(yield* provideContextServices(repositoryService.list(root))).map(
|
|
1021
|
+
(repository) => [repository.alias, repository],
|
|
1022
|
+
),
|
|
895
1023
|
)
|
|
896
1024
|
|
|
897
1025
|
const inspectCheckout = (
|