@markjaquith/agency 2.71.19 → 2.71.20

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.19",
3
+ "version": "2.71.20",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -70,6 +70,7 @@
70
70
  "benchmark:status": "bun scripts/benchmark-status.ts",
71
71
  "benchmark:next": "bun scripts/benchmark-next.ts",
72
72
  "benchmark:init": "bun scripts/benchmark-init.ts",
73
+ "benchmark:integration": "bun scripts/benchmark-integration.ts",
73
74
  "benchmark:workbase": "bun scripts/benchmark-workbase.ts",
74
75
  "benchmark:doctor": "bun scripts/benchmark-doctor.ts",
75
76
  "benchmark:context": "bun scripts/benchmark-context.ts",
@@ -18,7 +18,7 @@ export const init = (options: InitOptions = {}) =>
18
18
  const root = yield* workbase.initialize(
19
19
  options.path ? resolve(cwd, options.path) : cwd,
20
20
  )
21
- yield* integrations.sync(root)
21
+ yield* integrations.syncRoot(root)
22
22
  log(
23
23
  options.json
24
24
  ? JSON.stringify({ root }, null, 2)
@@ -258,6 +258,18 @@ const createHarness = (options: HarnessOptions = {}) => {
258
258
  ],
259
259
  })
260
260
  },
261
+ syncRoot: () => {
262
+ integrationSyncs += 1
263
+ return Effect.succeed({
264
+ root: "/workbase",
265
+ files: [
266
+ {
267
+ name: "opencode",
268
+ state: "managed",
269
+ },
270
+ ],
271
+ })
272
+ },
261
273
  }
262
274
  const fs = {
263
275
  isDirectory: (path: string) =>
@@ -130,7 +130,7 @@ export const work = (
130
130
  const inputAllowed = options.inputAllowed ?? true
131
131
  const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
132
132
  if (!root) return
133
- yield* integrations.sync(root)
133
+ yield* integrations.syncRoot(root)
134
134
  const { config } = yield* workbase.loadConfig(root)
135
135
  const globalConfig = yield* workbase.loadGlobalConfig()
136
136
 
@@ -91,6 +91,38 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
91
91
  catch: () => new FileNotFoundError({ path }),
92
92
  }),
93
93
 
94
+ inspectFile: (path: string) =>
95
+ Effect.tryPromise({
96
+ try: async () => {
97
+ try {
98
+ const stats = await lstat(path)
99
+ if (stats.isSymbolicLink()) {
100
+ return { kind: "symlink" as const }
101
+ }
102
+ if (!stats.isFile()) return { kind: "other" as const }
103
+ return {
104
+ kind: "file" as const,
105
+ content: await Bun.file(path).text(),
106
+ }
107
+ } catch (error) {
108
+ if (
109
+ typeof error === "object" &&
110
+ error !== null &&
111
+ "code" in error &&
112
+ error.code === "ENOENT"
113
+ ) {
114
+ return { kind: "missing" as const }
115
+ }
116
+ throw error
117
+ }
118
+ },
119
+ catch: (error) =>
120
+ new FileSystemError({
121
+ message: `Failed to inspect file: ${path}`,
122
+ cause: error,
123
+ }),
124
+ }),
125
+
94
126
  writeFile: (path: string, content: string) =>
95
127
  Effect.tryPromise({
96
128
  try: () => Bun.write(path, content),
@@ -17,6 +17,7 @@ import {
17
17
  managedWorkbaseOpencodeTuiPlugin,
18
18
  } from "../workbase/opencode-tui-plugin-file"
19
19
  import { IntegrationService } from "./IntegrationService"
20
+ import { FileSystemService } from "./FileSystemService"
20
21
 
21
22
  const write = async (root: string, path: string, content: string) => {
22
23
  const fullPath = join(root, path)
@@ -90,6 +91,54 @@ describe("IntegrationService", () => {
90
91
  ])
91
92
  })
92
93
 
94
+ test("inspects each integration path once per status call", async () => {
95
+ const service = await Effect.runPromise(
96
+ Effect.provide(FileSystemService, FileSystemService.Default),
97
+ )
98
+ const inspected = new Map<string, number>()
99
+ const instrumented = {
100
+ ...service,
101
+ inspectFile: (path: string) => {
102
+ inspected.set(path, (inspected.get(path) ?? 0) + 1)
103
+ return service.inspectFile(path)
104
+ },
105
+ }
106
+
107
+ await runTestEffect(
108
+ IntegrationService.pipe(
109
+ Effect.flatMap((integration) => integration.statusRoot(root)),
110
+ Effect.provideService(FileSystemService, instrumented),
111
+ ),
112
+ )
113
+
114
+ expect(inspected.size).toBe(8)
115
+ expect([...inspected.values()]).toEqual(Array(8).fill(1))
116
+ })
117
+
118
+ test("inspects integration and legacy paths once per synchronized call", async () => {
119
+ const service = await Effect.runPromise(
120
+ Effect.provide(FileSystemService, FileSystemService.Default),
121
+ )
122
+ const inspected = new Map<string, number>()
123
+ const instrumented = {
124
+ ...service,
125
+ inspectFile: (path: string) => {
126
+ inspected.set(path, (inspected.get(path) ?? 0) + 1)
127
+ return service.inspectFile(path)
128
+ },
129
+ }
130
+
131
+ await runTestEffect(
132
+ IntegrationService.pipe(
133
+ Effect.flatMap((integration) => integration.syncRoot(root)),
134
+ Effect.provideService(FileSystemService, instrumented),
135
+ ),
136
+ )
137
+
138
+ expect(inspected.size).toBe(11)
139
+ expect([...inspected.values()]).toEqual(Array(11).fill(1))
140
+ })
141
+
93
142
  test("reports customized and checksum-safe drifted files", async () => {
94
143
  await write(root, ".agency/AGENTS.md", "# Custom instructions\n")
95
144
  await write(
@@ -202,35 +202,54 @@ const inspect = (root: string) =>
202
202
  "agency-repository-skills.ts",
203
203
  )
204
204
  const tuiPluginPath = join(opencodeDirectory, "tui", "agency-debug.ts")
205
+ const [
206
+ agents,
207
+ opencode,
208
+ opencodeJson,
209
+ plugin,
210
+ legacyPlugin,
211
+ tui,
212
+ tuiJson,
213
+ tuiPlugin,
214
+ ] = yield* Effect.all(
215
+ [
216
+ fs.inspectFile(agentsPath),
217
+ fs.inspectFile(opencodePath),
218
+ fs.inspectFile(opencodeJsonPath),
219
+ fs.inspectFile(pluginPath),
220
+ fs.inspectFile(legacyPluginPath),
221
+ fs.inspectFile(tuiPath),
222
+ fs.inspectFile(tuiJsonPath),
223
+ fs.inspectFile(tuiPluginPath),
224
+ ] as const,
225
+ { concurrency: 8 },
226
+ )
205
227
  const files: IntegrationFileStatus[] = []
206
228
 
207
229
  files.push(
208
- (yield* fs.readSymlinkTarget(agentsPath)) !== null
230
+ agents.kind === "symlink"
209
231
  ? fileStatus("agents", agentsPath, "customized")
210
- : (yield* fs.exists(agentsPath))
232
+ : agents.kind === "file"
211
233
  ? classify(
212
234
  "agents",
213
235
  agentsPath,
214
- yield* fs.readFile(agentsPath),
236
+ agents.content,
215
237
  managedWorkbaseAgents,
216
238
  canUpdateManagedWorkbaseAgents,
217
239
  )
218
240
  : fileStatus("agents", agentsPath, "missing"),
219
241
  )
220
242
 
221
- if ((yield* fs.readSymlinkTarget(opencodePath)) !== null) {
243
+ if (opencode.kind === "symlink") {
222
244
  files.push(fileStatus("opencode", opencodePath, "customized"))
223
- } else if (
224
- (yield* fs.readSymlinkTarget(opencodeJsonPath)) !== null ||
225
- (yield* fs.exists(opencodeJsonPath))
226
- ) {
245
+ } else if (opencodeJson.kind !== "missing") {
227
246
  files.push(fileStatus("opencode", opencodeJsonPath, "customized"))
228
- } else if (yield* fs.exists(opencodePath)) {
247
+ } else if (opencode.kind === "file") {
229
248
  files.push(
230
249
  classify(
231
250
  "opencode",
232
251
  opencodePath,
233
- yield* fs.readFile(opencodePath),
252
+ opencode.content,
234
253
  managedWorkbaseOpencode,
235
254
  canUpdateManagedWorkbaseOpencode,
236
255
  ),
@@ -239,26 +258,21 @@ const inspect = (root: string) =>
239
258
  files.push(fileStatus("opencode", opencodePath, "missing"))
240
259
  }
241
260
 
242
- if ((yield* fs.readSymlinkTarget(pluginPath)) !== null) {
261
+ if (plugin.kind === "symlink") {
243
262
  files.push(fileStatus("opencode-plugin", pluginPath, "customized"))
244
- } else if (yield* fs.exists(pluginPath)) {
263
+ } else if (plugin.kind === "file") {
245
264
  files.push(
246
265
  classify(
247
266
  "opencode-plugin",
248
267
  pluginPath,
249
- yield* fs.readFile(pluginPath),
268
+ plugin.content,
250
269
  managedWorkbaseOpencodePlugin,
251
270
  canUpdateManagedWorkbaseOpencodePlugin,
252
271
  ),
253
272
  )
254
- } else if (
255
- (yield* fs.readSymlinkTarget(legacyPluginPath)) !== null ||
256
- (yield* fs.exists(legacyPluginPath))
257
- ) {
273
+ } else if (legacyPlugin.kind !== "missing") {
258
274
  const legacyContent =
259
- (yield* fs.readSymlinkTarget(legacyPluginPath)) === null
260
- ? yield* fs.readFile(legacyPluginPath)
261
- : null
275
+ legacyPlugin.kind === "file" ? legacyPlugin.content : null
262
276
  files.push(
263
277
  legacyContent !== null &&
264
278
  canUpdateManagedWorkbaseOpencodePlugin(legacyContent)
@@ -269,19 +283,16 @@ const inspect = (root: string) =>
269
283
  files.push(fileStatus("opencode-plugin", pluginPath, "missing"))
270
284
  }
271
285
 
272
- if ((yield* fs.readSymlinkTarget(tuiPath)) !== null) {
286
+ if (tui.kind === "symlink") {
273
287
  files.push(fileStatus("opencode-tui", tuiPath, "customized"))
274
- } else if (
275
- (yield* fs.readSymlinkTarget(tuiJsonPath)) !== null ||
276
- (yield* fs.exists(tuiJsonPath))
277
- ) {
288
+ } else if (tuiJson.kind !== "missing") {
278
289
  files.push(fileStatus("opencode-tui", tuiJsonPath, "customized"))
279
- } else if (yield* fs.exists(tuiPath)) {
290
+ } else if (tui.kind === "file") {
280
291
  files.push(
281
292
  classify(
282
293
  "opencode-tui",
283
294
  tuiPath,
284
- yield* fs.readFile(tuiPath),
295
+ tui.content,
285
296
  managedWorkbaseOpencodeTui,
286
297
  canUpdateManagedWorkbaseOpencodeTui,
287
298
  ),
@@ -290,14 +301,14 @@ const inspect = (root: string) =>
290
301
  files.push(fileStatus("opencode-tui", tuiPath, "missing"))
291
302
  }
292
303
 
293
- if ((yield* fs.readSymlinkTarget(tuiPluginPath)) !== null) {
304
+ if (tuiPlugin.kind === "symlink") {
294
305
  files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "customized"))
295
- } else if (yield* fs.exists(tuiPluginPath)) {
306
+ } else if (tuiPlugin.kind === "file") {
296
307
  files.push(
297
308
  classify(
298
309
  "opencode-tui-plugin",
299
310
  tuiPluginPath,
300
- yield* fs.readFile(tuiPluginPath),
311
+ tuiPlugin.content,
301
312
  managedWorkbaseOpencodeTuiPlugin,
302
313
  canUpdateManagedWorkbaseOpencodeTuiPlugin,
303
314
  ),
@@ -306,32 +317,25 @@ const inspect = (root: string) =>
306
317
  files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "missing"))
307
318
  }
308
319
 
309
- return files
320
+ return { files, legacyPlugin }
310
321
  })
311
322
 
312
323
  const canRemoveLegacyAgents = (root: string) =>
313
324
  Effect.gen(function* () {
314
325
  const fs = yield* FileSystemService
315
326
  const path = join(root, "AGENTS.md")
316
- if (
317
- (yield* fs.readSymlinkTarget(path)) !== null ||
318
- !(yield* fs.exists(path))
319
- )
320
- return false
321
- return canUpdateManagedWorkbaseAgents(yield* fs.readFile(path))
327
+ const file = yield* fs.inspectFile(path)
328
+ return file.kind === "file" && canUpdateManagedWorkbaseAgents(file.content)
322
329
  })
323
330
 
324
331
  const canRemoveLegacyOpencodeCommand = (root: string) =>
325
332
  Effect.gen(function* () {
326
333
  const fs = yield* FileSystemService
327
334
  const path = join(root, ".opencode", "command", "agency.md")
328
- if (
329
- (yield* fs.readSymlinkTarget(path)) !== null ||
330
- !(yield* fs.exists(path))
331
- )
332
- return false
335
+ const file = yield* fs.inspectFile(path)
336
+ if (file.kind !== "file") return false
333
337
 
334
- const content = yield* fs.readFile(path)
338
+ const content = file.content
335
339
  const header = /^---\r?\n# agency-managed: sha256=([a-f0-9]{64})\r?\n/
336
340
  const match = content.match(header)
337
341
  if (!match?.[1]) return false
@@ -340,23 +344,6 @@ const canRemoveLegacyOpencodeCommand = (root: string) =>
340
344
  return createHash("sha256").update(canonical).digest("hex") === match[1]
341
345
  })
342
346
 
343
- const canRemoveLegacyOpencodePlugin = (root: string) =>
344
- Effect.gen(function* () {
345
- const fs = yield* FileSystemService
346
- const path = join(
347
- root,
348
- ".opencode",
349
- "plugin",
350
- "agency-repository-skills.ts",
351
- )
352
- if (
353
- (yield* fs.readSymlinkTarget(path)) !== null ||
354
- !(yield* fs.exists(path))
355
- )
356
- return false
357
- return canUpdateManagedWorkbaseOpencodePlugin(yield* fs.readFile(path))
358
- })
359
-
360
347
  export class IntegrationService extends Effect.Service<IntegrationService>()(
361
348
  "IntegrationService",
362
349
  {
@@ -365,34 +352,59 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
365
352
  Effect.gen(function* () {
366
353
  const workbase = yield* WorkbaseService
367
354
  const root = yield* workbase.discover(startPath)
368
- return { root, files: yield* inspect(root) }
355
+ return { root, files: (yield* inspect(root)).files }
369
356
  }),
370
357
 
358
+ statusRoot: (root: string) =>
359
+ inspect(root).pipe(Effect.map(({ files }) => ({ root, files }))),
360
+
371
361
  sync: (startPath: string = process.cwd()) =>
372
362
  Effect.gen(function* () {
373
- const fs = yield* FileSystemService
374
363
  const workbase = yield* WorkbaseService
375
364
  const root = yield* workbase.discover(startPath)
376
- const statuses = yield* inspect(root)
377
- const removeLegacyAgents =
378
- statuses.some(
379
- (status) =>
380
- status.name === "opencode" && status.state !== "customized",
381
- ) && (yield* canRemoveLegacyAgents(root))
382
- const removeLegacyOpencodeCommand =
383
- yield* canRemoveLegacyOpencodeCommand(root)
384
- const removeLegacyOpencodePlugin =
385
- yield* canRemoveLegacyOpencodePlugin(root)
365
+ const service = yield* IntegrationService
366
+ return yield* service.syncRoot(root)
367
+ }),
368
+
369
+ syncRoot: (root: string) =>
370
+ Effect.gen(function* () {
371
+ const fs = yield* FileSystemService
372
+ const { files: statuses, legacyPlugin } = yield* inspect(root)
386
373
  const legacyPiExtension = join(
387
374
  root,
388
375
  ".pi",
389
376
  "extensions",
390
377
  "agency-workbase.ts",
391
378
  )
392
- const removeLegacyPiExtension =
393
- (yield* fs.readSymlinkTarget(legacyPiExtension)) === null &&
394
- (yield* fs.exists(legacyPiExtension)) &&
395
- canRemoveLegacyPiExtension(yield* fs.readFile(legacyPiExtension))
379
+ const canRemoveAgents = statuses.some(
380
+ (status) =>
381
+ status.name === "opencode" && status.state !== "customized",
382
+ )
383
+ const [
384
+ removeLegacyAgents,
385
+ removeLegacyOpencodeCommand,
386
+ removeLegacyPiExtension,
387
+ ] = yield* Effect.all(
388
+ [
389
+ canRemoveAgents
390
+ ? canRemoveLegacyAgents(root)
391
+ : Effect.succeed(false),
392
+ canRemoveLegacyOpencodeCommand(root),
393
+ fs
394
+ .inspectFile(legacyPiExtension)
395
+ .pipe(
396
+ Effect.map(
397
+ (file) =>
398
+ file.kind === "file" &&
399
+ canRemoveLegacyPiExtension(file.content),
400
+ ),
401
+ ),
402
+ ] as const,
403
+ { concurrency: 3 },
404
+ )
405
+ const removeLegacyOpencodePlugin =
406
+ legacyPlugin.kind === "file" &&
407
+ canUpdateManagedWorkbaseOpencodePlugin(legacyPlugin.content)
396
408
  const files: IntegrationSyncFile[] = []
397
409
 
398
410
  for (const status of statuses) {