@markjaquith/agency 2.66.0 → 2.67.0

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/README.md CHANGED
@@ -54,7 +54,7 @@ workbase/
54
54
  .opencode/
55
55
  opencode.jsonc # managed @agency subagent, instructions, and reference
56
56
  tui.jsonc # managed TUI plugin registration
57
- plugin/agency-repository-skills.ts # managed workbase access and checkout skills
57
+ plugins/agency-repository-skills.ts # managed workbase access and checkout skills
58
58
  tui/agency-debug.ts # managed /agency-debug TUI diagnostic
59
59
  agency.json # tracked config and portable repository declarations
60
60
  repos/ # ignored local materializations
@@ -119,6 +119,9 @@ toast and does not submit a prompt to an LLM. When no writable checkout skill
119
119
  directory is available, server initialization is reported as indeterminate
120
120
  rather than inferred from plugin discovery.
121
121
  OpenCode discovers the config and plugin from task and epic launch directories.
122
+ The plugin uses OpenCode's plural discovery directory and exports both the V1
123
+ server function and the `opencode2` module wrapper. Integration sync migrates a
124
+ checksum-valid legacy singular-path plugin and preserves customized files.
122
125
  The plugin grants whole-workbase access dynamically, while the portable
123
126
  reference advertises that context to agents. Bash and Agency operations must
124
127
  still follow the write authority reported by `agency context`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.66.0",
3
+ "version": "2.67.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -74,7 +74,7 @@ describe("init command", () => {
74
74
  await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
75
75
  ).toBe(false)
76
76
  const plugin = await Bun.file(
77
- join(root, ".opencode/plugin/agency-repository-skills.ts"),
77
+ join(root, ".opencode/plugins/agency-repository-skills.ts"),
78
78
  ).text()
79
79
  expect(plugin).toContain("AGENCY_WRITABLE_CHECKOUT")
80
80
  expect(plugin).toContain("config.skills.paths")
@@ -87,7 +87,7 @@ OpenCode config: missing
87
87
  Action: Run 'agency integration sync' to install Agency instructions and whole-workbase OpenCode access.
88
88
 
89
89
  OpenCode workbase plugin: missing
90
- Path: .opencode/plugin/agency-repository-skills.ts
90
+ Path: .opencode/plugins/agency-repository-skills.ts
91
91
  The managed OpenCode workbase plugin needs synchronization.
92
92
  Action: Run 'agency integration sync' to provide workbase access and expose writable-checkout skills in OpenCode.
93
93
 
@@ -124,7 +124,7 @@ OpenCode /agency-debug: missing
124
124
  ).toBe(false)
125
125
  expect(
126
126
  await Bun.file(
127
- join(root, ".opencode/plugin/agency-repository-skills.ts"),
127
+ join(root, ".opencode/plugins/agency-repository-skills.ts"),
128
128
  ).exists(),
129
129
  ).toBe(true)
130
130
  expect(await Bun.file(join(root, ".opencode/tui.jsonc")).exists()).toBe(
@@ -151,7 +151,7 @@ OpenCode config: synced
151
151
  Agency's managed OpenCode launch config is ready to load Agency instructions and provide whole-workbase read access.
152
152
 
153
153
  OpenCode workbase plugin: synced
154
- Path: .opencode/plugin/agency-repository-skills.ts
154
+ Path: .opencode/plugins/agency-repository-skills.ts
155
155
  Agency's managed OpenCode plugin provides whole-workbase access and exposes writable-checkout skills.
156
156
 
157
157
  OpenCode TUI config: synced
@@ -72,7 +72,7 @@ describe("IntegrationService", () => {
72
72
  await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
73
73
  await write(
74
74
  root,
75
- ".opencode/plugin/agency-repository-skills.ts",
75
+ ".opencode/plugins/agency-repository-skills.ts",
76
76
  managedWorkbaseOpencodePlugin,
77
77
  )
78
78
  await write(root, ".opencode/tui.jsonc", managedWorkbaseOpencodeTui)
@@ -126,6 +126,15 @@ describe("IntegrationService", () => {
126
126
  expect(managedWorkbaseOpencodePlugin).toContain(
127
127
  "process.env.AGENCY_WRITABLE_CHECKOUT",
128
128
  )
129
+ expect(managedWorkbaseOpencodePlugin).toContain(
130
+ 'import type { Plugin, PluginModule } from "@opencode-ai/plugin/v1"',
131
+ )
132
+ expect(managedWorkbaseOpencodePlugin).toContain(
133
+ "export const AgencyPlugin = plugin",
134
+ )
135
+ expect(managedWorkbaseOpencodePlugin).toContain('id: "agency"')
136
+ expect(managedWorkbaseOpencodePlugin).toContain("setup,")
137
+ expect(managedWorkbaseOpencodePlugin).toContain("server: plugin")
129
138
  expect(managedWorkbaseOpencodePlugin).toContain(
130
139
  '["agency", "context", ".", "--compact", "--json"]',
131
140
  )
@@ -276,7 +285,12 @@ describe("IntegrationService", () => {
276
285
  task: "example",
277
286
  phase,
278
287
  })
279
- const hooks = await generated.default({ directory: root } as never)
288
+ expect(generated.default).toMatchObject({
289
+ id: "agency",
290
+ setup: expect.any(Function),
291
+ server: generated.AgencyPlugin,
292
+ })
293
+ const hooks = await generated.AgencyPlugin({ directory: root } as never)
280
294
  await hooks["chat.message"]!(
281
295
  { sessionID: "worker-session" } as never,
282
296
  {
@@ -351,6 +365,52 @@ describe("IntegrationService", () => {
351
365
  phase: "build",
352
366
  }))
353
367
 
368
+ test("registers the workbase reference through the OpenCode V2 API", async () => {
369
+ const path = join(root, ".opencode/plugins/agency-repository-skills.ts")
370
+ await write(
371
+ root,
372
+ ".opencode/plugins/agency-repository-skills.ts",
373
+ managedWorkbaseOpencodePlugin,
374
+ )
375
+ const generated = await import(`${pathToFileURL(path).href}?v2-setup`)
376
+ let reference:
377
+ | {
378
+ name: string
379
+ source: { type: string; path: string; description: string }
380
+ }
381
+ | undefined
382
+
383
+ await generated.default.setup({
384
+ reference: {
385
+ transform: async (callback: (references: unknown) => void) =>
386
+ callback({
387
+ list: () => [],
388
+ add: (
389
+ name: string,
390
+ source: {
391
+ type: string
392
+ path: string
393
+ description: string
394
+ },
395
+ ) => {
396
+ reference = { name, source }
397
+ },
398
+ }),
399
+ },
400
+ skill: { transform: async () => {} },
401
+ })
402
+
403
+ expect(reference).toEqual({
404
+ name: "workbase",
405
+ source: {
406
+ type: "local",
407
+ path: root,
408
+ description:
409
+ "Complete Agency workbase context; write authority still comes only from agency context",
410
+ },
411
+ })
412
+ })
413
+
354
414
  test("registers a TUI-only /agency-debug diagnostic", async () => {
355
415
  const config = JSON.parse(managedBody(managedWorkbaseOpencodeTui))
356
416
  expect(config).toEqual({
@@ -688,7 +748,7 @@ describe("IntegrationService", () => {
688
748
  ).toBe(false)
689
749
  expect(
690
750
  await Bun.file(
691
- join(root, ".opencode/plugin/agency-repository-skills.ts"),
751
+ join(root, ".opencode/plugins/agency-repository-skills.ts"),
692
752
  ).text(),
693
753
  ).toBe(managedWorkbaseOpencodePlugin)
694
754
  expect(await Bun.file(join(root, ".opencode/tui.jsonc")).text()).toBe(
@@ -761,6 +821,35 @@ describe("IntegrationService", () => {
761
821
  })
762
822
  })
763
823
 
824
+ test("migrates a checksum-valid plugin from the legacy singular path", async () => {
825
+ const legacyPath = join(
826
+ root,
827
+ ".opencode/plugin/agency-repository-skills.ts",
828
+ )
829
+ const pluginPath = join(
830
+ root,
831
+ ".opencode/plugins/agency-repository-skills.ts",
832
+ )
833
+ await write(
834
+ root,
835
+ ".opencode/plugin/agency-repository-skills.ts",
836
+ managedWorkbaseOpencodePlugin,
837
+ )
838
+
839
+ const result = await sync(root)
840
+
841
+ expect(result.files[2]).toMatchObject({
842
+ name: "opencode-plugin",
843
+ path: pluginPath,
844
+ state: "managed",
845
+ changed: true,
846
+ })
847
+ expect(await Bun.file(pluginPath).text()).toBe(
848
+ managedWorkbaseOpencodePlugin,
849
+ )
850
+ expect(await Bun.file(legacyPath).exists()).toBe(false)
851
+ })
852
+
764
853
  test("preserves user-owned TUI config and diagnostic plugin", async () => {
765
854
  const customConfig = '{"theme":"custom"}\n'
766
855
  const customPlugin =
@@ -179,12 +179,12 @@ const inspect = (root: string) =>
179
179
  const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
180
180
  const tuiPath = join(opencodeDirectory, "tui.jsonc")
181
181
  const tuiJsonPath = join(opencodeDirectory, "tui.json")
182
- const pluginPath = join(
182
+ const legacyPluginPath = join(
183
183
  opencodeDirectory,
184
184
  "plugin",
185
185
  "agency-repository-skills.ts",
186
186
  )
187
- const pluralPluginPath = join(
187
+ const pluginPath = join(
188
188
  opencodeDirectory,
189
189
  "plugins",
190
190
  "agency-repository-skills.ts",
@@ -229,11 +229,6 @@ const inspect = (root: string) =>
229
229
 
230
230
  if ((yield* fs.readSymlinkTarget(pluginPath)) !== null) {
231
231
  files.push(fileStatus("opencode-plugin", pluginPath, "customized"))
232
- } else if (
233
- (yield* fs.readSymlinkTarget(pluralPluginPath)) !== null ||
234
- (yield* fs.exists(pluralPluginPath))
235
- ) {
236
- files.push(fileStatus("opencode-plugin", pluralPluginPath, "customized"))
237
232
  } else if (yield* fs.exists(pluginPath)) {
238
233
  files.push(
239
234
  classify(
@@ -244,6 +239,20 @@ const inspect = (root: string) =>
244
239
  canUpdateManagedWorkbaseOpencodePlugin,
245
240
  ),
246
241
  )
242
+ } else if (
243
+ (yield* fs.readSymlinkTarget(legacyPluginPath)) !== null ||
244
+ (yield* fs.exists(legacyPluginPath))
245
+ ) {
246
+ const legacyContent =
247
+ (yield* fs.readSymlinkTarget(legacyPluginPath)) === null
248
+ ? yield* fs.readFile(legacyPluginPath)
249
+ : null
250
+ files.push(
251
+ legacyContent !== null &&
252
+ canUpdateManagedWorkbaseOpencodePlugin(legacyContent)
253
+ ? fileStatus("opencode-plugin", pluginPath, "missing")
254
+ : fileStatus("opencode-plugin", legacyPluginPath, "customized"),
255
+ )
247
256
  } else {
248
257
  files.push(fileStatus("opencode-plugin", pluginPath, "missing"))
249
258
  }
@@ -319,6 +328,23 @@ const canRemoveLegacyOpencodeCommand = (root: string) =>
319
328
  return createHash("sha256").update(canonical).digest("hex") === match[1]
320
329
  })
321
330
 
331
+ const canRemoveLegacyOpencodePlugin = (root: string) =>
332
+ Effect.gen(function* () {
333
+ const fs = yield* FileSystemService
334
+ const path = join(
335
+ root,
336
+ ".opencode",
337
+ "plugin",
338
+ "agency-repository-skills.ts",
339
+ )
340
+ if (
341
+ (yield* fs.readSymlinkTarget(path)) !== null ||
342
+ !(yield* fs.exists(path))
343
+ )
344
+ return false
345
+ return canUpdateManagedWorkbaseOpencodePlugin(yield* fs.readFile(path))
346
+ })
347
+
322
348
  export class IntegrationService extends Effect.Service<IntegrationService>()(
323
349
  "IntegrationService",
324
350
  {
@@ -343,6 +369,8 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
343
369
  ) && (yield* canRemoveLegacyAgents(root))
344
370
  const removeLegacyOpencodeCommand =
345
371
  yield* canRemoveLegacyOpencodeCommand(root)
372
+ const removeLegacyOpencodePlugin =
373
+ yield* canRemoveLegacyOpencodePlugin(root)
346
374
  const files: IntegrationSyncFile[] = []
347
375
 
348
376
  for (const status of statuses) {
@@ -356,7 +384,7 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
356
384
  yield* fs.createDirectory(join(root, ".opencode"))
357
385
  yield* fs.writeFile(status.path, managedWorkbaseOpencode)
358
386
  } else if (status.name === "opencode-plugin") {
359
- yield* fs.createDirectory(join(root, ".opencode", "plugin"))
387
+ yield* fs.createDirectory(join(root, ".opencode", "plugins"))
360
388
  yield* fs.writeFile(status.path, managedWorkbaseOpencodePlugin)
361
389
  } else if (status.name === "opencode-tui") {
362
390
  yield* fs.createDirectory(join(root, ".opencode"))
@@ -376,11 +404,19 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
376
404
  needsWrite ? "managed" : status.state,
377
405
  ),
378
406
  changed:
379
- needsWrite || (status.name === "agents" && removeLegacyAgents),
407
+ needsWrite ||
408
+ (status.name === "agents" && removeLegacyAgents) ||
409
+ (status.name === "opencode-plugin" &&
410
+ removeLegacyOpencodePlugin),
380
411
  })
381
412
  }
382
413
 
383
414
  if (removeLegacyAgents) yield* fs.deleteFile(join(root, "AGENTS.md"))
415
+ if (removeLegacyOpencodePlugin) {
416
+ yield* fs.deleteFile(
417
+ join(root, ".opencode", "plugin", "agency-repository-skills.ts"),
418
+ )
419
+ }
384
420
  if (removeLegacyOpencodeCommand) {
385
421
  yield* fs.deleteFile(
386
422
  join(root, ".opencode", "command", "agency.md"),
@@ -8,7 +8,8 @@ const checksum = (content: string) =>
8
8
 
9
9
  const body = `import { existsSync, readFileSync } from "node:fs"
10
10
  import { dirname, join, resolve, sep } from "node:path"
11
- import type { Plugin } from "@opencode-ai/plugin"
11
+ import { fileURLToPath } from "node:url"
12
+ import type { Plugin, PluginModule } from "@opencode-ai/plugin/v1"
12
13
 
13
14
  const workerLaunchPattern = /^Agency worker launch target: ([^.\\s]+)\\./
14
15
 
@@ -20,6 +21,20 @@ type AgencyContext = {
20
21
  phase?: string
21
22
  }
22
23
 
24
+ type V2PluginContext = {
25
+ reference: {
26
+ transform(callback: (references: {
27
+ list(): readonly (readonly [string, unknown])[]
28
+ add(name: string, source: { type: "local"; path: string; description: string }): void
29
+ }) => void): Promise<unknown>
30
+ }
31
+ skill: {
32
+ transform(callback: (skills: {
33
+ source(source: { type: "directory"; path: string }): void
34
+ }) => void): Promise<unknown>
35
+ }
36
+ }
37
+
23
38
  const contextTarget = (result: Record<string, any>): string | undefined => {
24
39
  const target = result.target
25
40
  if (target?.kind === "epic") return \`epic:\${target.epicId}\`
@@ -69,6 +84,15 @@ const discoverCheckout = (directory: string, root: string | undefined) => {
69
84
  }
70
85
  }
71
86
 
87
+ const checkoutSkillPaths = (checkout: string | undefined) => checkout
88
+ ? [
89
+ join(checkout, ".claude", "skills"),
90
+ join(checkout, ".agents", "skills"),
91
+ join(checkout, ".opencode", "skill"),
92
+ join(checkout, ".opencode", "skills"),
93
+ ].filter(existsSync)
94
+ : []
95
+
72
96
  const agencyContext = async (
73
97
  directory: string,
74
98
  useEnvironmentTarget = true,
@@ -141,12 +165,7 @@ const plugin: Plugin = async ({ directory }) => {
141
165
 
142
166
  if (!checkout) return
143
167
 
144
- const paths = [
145
- join(checkout, ".claude", "skills"),
146
- join(checkout, ".agents", "skills"),
147
- join(checkout, ".opencode", "skill"),
148
- join(checkout, ".opencode", "skills"),
149
- ].filter(existsSync).map((path) => \`\${path}\${sep}.\`)
168
+ const paths = checkoutSkillPaths(checkout).map((path) => \`\${path}\${sep}.\`)
150
169
  if (paths.length === 0) return
151
170
 
152
171
  config.skills ??= {}
@@ -186,7 +205,31 @@ const plugin: Plugin = async ({ directory }) => {
186
205
  }
187
206
  }
188
207
 
189
- export default plugin
208
+ export const AgencyPlugin = plugin
209
+
210
+ const setup = async (context: V2PluginContext) => {
211
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..")
212
+ await context.reference.transform((references) => {
213
+ if (references.list().some(([name]) => name === "workbase")) return
214
+ references.add("workbase", {
215
+ type: "local",
216
+ path: root,
217
+ description: "Complete Agency workbase context; write authority still comes only from agency context",
218
+ })
219
+ })
220
+
221
+ const paths = checkoutSkillPaths(process.env.AGENCY_WRITABLE_CHECKOUT)
222
+ if (paths.length === 0) return
223
+ await context.skill.transform((skills) => {
224
+ for (const path of paths) skills.source({ type: "directory", path })
225
+ })
226
+ }
227
+
228
+ export default {
229
+ id: "agency",
230
+ setup,
231
+ server: plugin,
232
+ } satisfies PluginModule & { setup: (context: V2PluginContext) => Promise<void> }
190
233
  `
191
234
 
192
235
  const renderManagedWorkbaseOpencodePlugin = (content: string) =>