@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
|
-
|
|
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
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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
|
|
182
|
+
const legacyPluginPath = join(
|
|
183
183
|
opencodeDirectory,
|
|
184
184
|
"plugin",
|
|
185
185
|
"agency-repository-skills.ts",
|
|
186
186
|
)
|
|
187
|
-
const
|
|
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", "
|
|
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 ||
|
|
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
|
|
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
|
|
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) =>
|