@markjaquith/agency 2.65.2 → 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`.
@@ -347,8 +350,10 @@ external effects the failed invocation may have completed.
347
350
 
348
351
  ### Agent Runners
349
352
 
350
- OpenCode and Claude Code are built-in runner presets. Select either preset or a
351
- configured runner with `agency work --runner <name>`. A launch is fresh unless
353
+ OpenCode (`opencode2` and `opencode`) and Claude Code are built-in runner presets.
354
+ Without an explicit runner, Agency uses the first available executable in this
355
+ order: `opencode2`, `opencode`, then `claude`. Select a preset or configured
356
+ runner with `agency work --runner <name>`. A launch is fresh unless
352
357
  `AGENCY_SESSION_ID` is already set; resumed launches use the runner's
353
358
  `resumeCommand` when configured. The built-in presets use `--continue` only for
354
359
  resumed launches. By default Agency opens the runner without a prompt. `--auto`
@@ -399,8 +404,9 @@ to the receiving OpenCode session, injects an explicit active-worker system
399
404
  instruction, and restores Agency identity for that session's shell environment.
400
405
  This session bridge is necessary because an OpenCode client can attach to a
401
406
  long-lived server process that did not inherit the client's launch environment.
402
- The `opencode` runner remains rooted in its task or epic working directory so
403
- the workbase `AGENTS.md` and managed OpenCode config are discovered normally.
407
+ The `opencode2` and `opencode` runners remain rooted in their task or epic
408
+ working directory so the workbase `AGENTS.md` and managed OpenCode config are
409
+ discovered normally.
404
410
  Agency's managed OpenCode plugin grants the active workbase external-directory
405
411
  access and adds existing checkout-local `.claude/skills`, `.agents/skills`, and
406
412
  `.opencode/{skill,skills}` directories to `skills.paths`.
@@ -1063,10 +1069,10 @@ configured external chooser. Pass a directory, including `.` for the current
1063
1069
  directory, to infer its epic, task, or phase. Outside a workbase, Agency first
1064
1070
  presents the registered workbases, then the selected workbase's hierarchy.
1065
1071
 
1066
- OpenCode is the default runner, with automatic Claude fallback when neither is
1067
- explicitly selected. `--opencode` and `--claude` remain aliases for requiring
1068
- their built-in presets. Launches are interactive and promptless by default; use
1069
- `--auto` to send Agency's generated context prompt.
1072
+ Agency automatically uses the first available runner in this order: `opencode2`,
1073
+ `opencode`, then `claude`. `--opencode` and `--claude` remain aliases for
1074
+ requiring their corresponding built-in presets. Launches are interactive and
1075
+ promptless by default; use `--auto` to send Agency's generated context prompt.
1070
1076
 
1071
1077
  `agency work prepare` resolves an execution unit and creates or reuses its
1072
1078
  writable and reference worktrees, or its single pinned review checkout, without
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.65.2",
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
@@ -1050,18 +1050,45 @@ describe("work command", () => {
1050
1050
  ).toBeUndefined()
1051
1051
  })
1052
1052
 
1053
- test("automatically falls back to Claude", async () => {
1054
- const harness = createHarness({ available: { opencode: false } })
1053
+ test("automatically prefers opencode2", async () => {
1054
+ const harness = createHarness()
1055
+
1056
+ await harness.run({ taskId: "example" })
1057
+
1058
+ expect(harness.probes).toEqual(["opencode2"])
1059
+ expect(harness.launches[0]).toEqual({
1060
+ cli: "opencode2",
1061
+ args: ["opencode2"],
1062
+ cwd: taskDirectory,
1063
+ })
1064
+ })
1065
+
1066
+ test("automatically falls back from opencode2 to opencode", async () => {
1067
+ const harness = createHarness({ available: { opencode2: false } })
1055
1068
 
1056
1069
  await harness.run({ taskId: "example" })
1057
1070
 
1058
- expect(harness.probes).toEqual(["opencode", "claude"])
1071
+ expect(harness.probes).toEqual(["opencode2", "opencode"])
1072
+ expect(harness.launches[0]).toEqual({
1073
+ cli: "opencode",
1074
+ args: ["opencode"],
1075
+ cwd: taskDirectory,
1076
+ })
1077
+ })
1078
+
1079
+ test("automatically falls back from OpenCode to Claude", async () => {
1080
+ const harness = createHarness({
1081
+ available: { opencode2: false, opencode: false },
1082
+ })
1083
+
1084
+ await harness.run({ taskId: "example" })
1085
+
1086
+ expect(harness.probes).toEqual(["opencode2", "opencode", "claude"])
1059
1087
  expect(harness.launches[0]).toEqual({
1060
1088
  cli: "claude",
1061
1089
  args: ["claude"],
1062
1090
  cwd: taskDirectory,
1063
1091
  })
1064
- expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
1065
1092
  })
1066
1093
 
1067
1094
  test("does not fall back when OpenCode is explicitly required", async () => {
@@ -1090,13 +1117,13 @@ describe("work command", () => {
1090
1117
 
1091
1118
  test("fails when neither agent tool is available", async () => {
1092
1119
  const harness = createHarness({
1093
- available: { opencode: false, claude: false },
1120
+ available: { opencode2: false, opencode: false, claude: false },
1094
1121
  })
1095
1122
 
1096
1123
  await expect(harness.run({ taskId: "example" })).rejects.toThrow(
1097
1124
  "claude CLI tool not found",
1098
1125
  )
1099
- expect(harness.probes).toEqual(["opencode", "claude"])
1126
+ expect(harness.probes).toEqual(["opencode2", "opencode", "claude"])
1100
1127
  expect(harness.launches).toEqual([])
1101
1128
  })
1102
1129
 
@@ -1150,7 +1177,7 @@ describe("work command", () => {
1150
1177
  verboseHarness.run({ taskId: "example", verbose: true }),
1151
1178
  )
1152
1179
  expect(verboseLogs).toEqual([
1153
- "Launching command: opencode (cwd: /workbase/tasks/example)",
1180
+ "Launching command: opencode2 (cwd: /workbase/tasks/example)",
1154
1181
  ])
1155
1182
  expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
1156
1183
 
@@ -280,10 +280,16 @@ export const work = (
280
280
  options.claude ||
281
281
  process.env.AGENCY_RUNNER,
282
282
  )
283
- let runner =
283
+ const defaultRunners = ["opencode2", "opencode", "claude"] as const
284
+ let defaultRunnerIndex = 0
285
+ let runner: string =
284
286
  options.runner ??
285
287
  process.env.AGENCY_RUNNER ??
286
- (options.claude ? "claude" : "opencode")
288
+ (options.claude
289
+ ? "claude"
290
+ : options.opencode
291
+ ? "opencode"
292
+ : defaultRunners[defaultRunnerIndex]!)
287
293
  const claimant = process.env.AGENCY_CLAIMANT ?? process.env.USER ?? "agency"
288
294
  const sessionId =
289
295
  process.env.AGENCY_SESSION_ID ?? `${process.pid}-${Date.now()}`
@@ -311,12 +317,12 @@ export const work = (
311
317
  let available = yield* fs.runCommand(["which", cli], {
312
318
  captureOutput: true,
313
319
  })
314
- if (
320
+ while (
315
321
  available.exitCode !== 0 &&
316
322
  !explicitlyRequested &&
317
- runner === "opencode"
323
+ defaultRunnerIndex < defaultRunners.length - 1
318
324
  ) {
319
- runner = "claude"
325
+ runner = defaultRunners[++defaultRunnerIndex]!
320
326
  resolved = resolveRunnerCommand(
321
327
  runner,
322
328
  config.runners,
@@ -580,7 +586,8 @@ Launch an agent for an epic, task, or phase. With no directory, select one
580
586
  interactively. A positional argument resolves as a directory first, then as a task
581
587
  ID. Use '.' for the current directory. Outside a workbase, select a registered
582
588
  workbase first. Managed OpenCode launches receive whole-workbase access through
583
- Agency's project plugin; Agency context remains authoritative for writes.
589
+ Agency's project plugin; Agency context remains authoritative for writes. Automatic
590
+ runner discovery checks opencode2, opencode, then claude.
584
591
 
585
592
  The prepare subcommand resolves and materializes an execution workspace without
586
593
  launching an agent or changing lifecycle status. --dry-run reports planned Git
@@ -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) =>
@@ -19,6 +19,12 @@ const variables = {
19
19
 
20
20
  describe("runner commands", () => {
21
21
  test("uses promptless interactive commands for built-in presets", () => {
22
+ expect(
23
+ resolveRunnerCommand("opencode2", undefined, variables, false).argv,
24
+ ).toEqual(["opencode2"])
25
+ expect(
26
+ resolveRunnerCommand("opencode2", undefined, variables, true).argv,
27
+ ).toEqual(["opencode2", "--continue"])
22
28
  expect(
23
29
  resolveRunnerCommand("opencode", undefined, variables, false).argv,
24
30
  ).toEqual(["opencode"])
@@ -31,6 +37,12 @@ describe("runner commands", () => {
31
37
  })
32
38
 
33
39
  test("uses autonomous commands when a prompt is requested", () => {
40
+ expect(
41
+ resolveRunnerCommand("opencode2", undefined, variables, false, true).argv,
42
+ ).toEqual(["opencode2", "--prompt", "Read the task."])
43
+ expect(
44
+ resolveRunnerCommand("opencode2", undefined, variables, true, true).argv,
45
+ ).toEqual(["opencode2", "--continue", "--prompt", "Read the task."])
34
46
  expect(
35
47
  resolveRunnerCommand("opencode", undefined, variables, false, true).argv,
36
48
  ).toEqual(["opencode", "--prompt", "Read the task."])
@@ -31,6 +31,12 @@ const PLACEHOLDERS = new Set<keyof RunnerCommandVariables>([
31
31
  ])
32
32
 
33
33
  const BUILTIN_RUNNERS: Readonly<Record<string, RunnerDefinition>> = {
34
+ opencode2: {
35
+ command: ["opencode2"],
36
+ autoCommand: ["opencode2", "--prompt", "{prompt}"],
37
+ resumeCommand: ["opencode2", "--continue"],
38
+ autoResumeCommand: ["opencode2", "--continue", "--prompt", "{prompt}"],
39
+ },
34
40
  opencode: {
35
41
  command: ["opencode"],
36
42
  autoCommand: ["opencode", "--prompt", "{prompt}"],