@markjaquith/agency 2.66.0 → 2.68.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 +28 -10
- package/package.json +1 -1
- package/src/cli.test.ts +2 -0
- package/src/commands/init.test.ts +7 -1
- package/src/commands/integration.test.ts +23 -5
- package/src/commands/integration.ts +6 -4
- package/src/services/IntegrationService.test.ts +248 -3
- package/src/services/IntegrationService.ts +96 -10
- package/src/workbase/opencode-plugin-file.ts +51 -8
- package/src/workbase/pi-extension-file.ts +158 -0
package/README.md
CHANGED
|
@@ -54,8 +54,10 @@ 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
|
+
.pi/
|
|
60
|
+
extensions/agency-workbase.ts # managed workbase context and checkout skills
|
|
59
61
|
agency.json # tracked config and portable repository declarations
|
|
60
62
|
repos/ # ignored local materializations
|
|
61
63
|
frontend/ # bare Git repository or symlink
|
|
@@ -83,11 +85,11 @@ workbase/
|
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
Agency keeps discovery and other observational commands read-only. Run
|
|
86
|
-
`agency integration status` to inspect `.agency/AGENTS.md
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
`agency integration status` to inspect `.agency/AGENTS.md`, the managed
|
|
89
|
+
OpenCode configuration and plugins, and `.pi/extensions/agency-workbase.ts`,
|
|
90
|
+
then `agency integration sync` to create missing files or refresh checksum-safe
|
|
91
|
+
managed files. Customized files are reported but never overwritten. Sync also
|
|
92
|
+
removes checksum-valid retired managed artifacts while
|
|
91
93
|
preserving customized files at their former paths. The root
|
|
92
94
|
`AGENTS.md` is user-owned and is not inspected or modified by Agency.
|
|
93
95
|
|
|
@@ -119,10 +121,23 @@ toast and does not submit a prompt to an LLM. When no writable checkout skill
|
|
|
119
121
|
directory is available, server initialization is reported as indeterminate
|
|
120
122
|
rather than inferred from plugin discovery.
|
|
121
123
|
OpenCode discovers the config and plugin from task and epic launch directories.
|
|
124
|
+
The plugin uses OpenCode's plural discovery directory and exports both the V1
|
|
125
|
+
server function and the `opencode2` module wrapper. Integration sync migrates a
|
|
126
|
+
checksum-valid legacy singular-path plugin and preserves customized files.
|
|
122
127
|
The plugin grants whole-workbase access dynamically, while the portable
|
|
123
128
|
reference advertises that context to agents. Bash and Agency operations must
|
|
124
129
|
still follow the write authority reported by `agency context`.
|
|
125
130
|
|
|
131
|
+
Pi discovers the managed project extension automatically when launched from
|
|
132
|
+
the workbase root after the project is trusted. When launching Pi from a task,
|
|
133
|
+
phase, or epic directory, pass the managed file with `--extension`, for example
|
|
134
|
+
`pi -e /path/to/workbase/.pi/extensions/agency-workbase.ts`. The extension loads
|
|
135
|
+
the managed Agency instructions into Pi's system prompt, advertises the complete
|
|
136
|
+
workbase, and exposes skills from the writable checkout's `.claude/skills`,
|
|
137
|
+
`.agents/skills`, `.opencode/{skill,skills}`, and `.pi/skills` directories.
|
|
138
|
+
Agency context still determines write authority; reference checkouts remain
|
|
139
|
+
read-only.
|
|
140
|
+
|
|
126
141
|
Repository aliases, the version-control backend, and canonical fetch remotes are
|
|
127
142
|
declared in tracked `agency.json`; local clones and symlinks remain ignored under
|
|
128
143
|
`repos/{alias}`. A declaration contains no local path, symlink target, checkout,
|
|
@@ -406,11 +421,14 @@ working directory so the workbase `AGENTS.md` and managed OpenCode config are
|
|
|
406
421
|
discovered normally.
|
|
407
422
|
Agency's managed OpenCode plugin grants the active workbase external-directory
|
|
408
423
|
access and adds existing checkout-local `.claude/skills`, `.agents/skills`, and
|
|
409
|
-
`.opencode/{skill,skills}` directories to `skills.paths`.
|
|
410
|
-
|
|
411
|
-
|
|
424
|
+
`.opencode/{skill,skills}` directories to `skills.paths`. The managed Pi
|
|
425
|
+
extension provides equivalent whole-workbase context and additionally discovers
|
|
426
|
+
checkout-local `.pi/skills` through Pi's `resources_discover` lifecycle.
|
|
427
|
+
`agency work` supplies the checkout directly; plain OpenCode launches and Pi
|
|
428
|
+
launches with the managed extension loaded resolve a materialized execution-unit
|
|
429
|
+
checkout through `agency context`. A multi-phase
|
|
412
430
|
task root has no single checkout, so launch from its phase directory when using
|
|
413
|
-
plain OpenCode. Other checkout-local configuration is not composed.
|
|
431
|
+
plain OpenCode or Pi. Other checkout-local configuration is not composed.
|
|
414
432
|
`--print-command` prints the exact cwd and argv plus non-secret environment keys
|
|
415
433
|
without launching the runner.
|
|
416
434
|
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -642,6 +642,7 @@ exit 23
|
|
|
642
642
|
{ name: "opencode-plugin", state: "managed" },
|
|
643
643
|
{ name: "opencode-tui", state: "managed" },
|
|
644
644
|
{ name: "opencode-tui-plugin", state: "managed" },
|
|
645
|
+
{ name: "pi-extension", state: "managed" },
|
|
645
646
|
])
|
|
646
647
|
|
|
647
648
|
const synced = parseJson(
|
|
@@ -653,6 +654,7 @@ exit 23
|
|
|
653
654
|
{ name: "opencode-plugin", state: "managed", changed: false },
|
|
654
655
|
{ name: "opencode-tui", state: "managed", changed: false },
|
|
655
656
|
{ name: "opencode-tui-plugin", state: "managed", changed: false },
|
|
657
|
+
{ name: "pi-extension", state: "managed", changed: false },
|
|
656
658
|
])
|
|
657
659
|
})
|
|
658
660
|
|
|
@@ -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")
|
|
@@ -89,6 +89,12 @@ describe("init command", () => {
|
|
|
89
89
|
expect(tuiPlugin).toContain('slashName: "agency-debug"')
|
|
90
90
|
expect(tuiPlugin).toContain("api.ui.toast")
|
|
91
91
|
expect(tuiPlugin).not.toContain("chat.message")
|
|
92
|
+
const piExtension = await Bun.file(
|
|
93
|
+
join(root, ".pi/extensions/agency-workbase.ts"),
|
|
94
|
+
).text()
|
|
95
|
+
expect(piExtension).toContain('pi.on("resources_discover"')
|
|
96
|
+
expect(piExtension).toContain('pi.on("before_agent_start"')
|
|
97
|
+
expect(piExtension).toContain("AGENCY_WRITABLE_CHECKOUT")
|
|
92
98
|
})
|
|
93
99
|
|
|
94
100
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -51,6 +51,11 @@ describe("integration command", () => {
|
|
|
51
51
|
state: "missing",
|
|
52
52
|
remediation: expect.stringContaining("integration sync"),
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "pi-extension",
|
|
56
|
+
state: "missing",
|
|
57
|
+
remediation: expect.stringContaining("integration sync"),
|
|
58
|
+
},
|
|
54
59
|
],
|
|
55
60
|
})
|
|
56
61
|
})
|
|
@@ -87,7 +92,7 @@ OpenCode config: missing
|
|
|
87
92
|
Action: Run 'agency integration sync' to install Agency instructions and whole-workbase OpenCode access.
|
|
88
93
|
|
|
89
94
|
OpenCode workbase plugin: missing
|
|
90
|
-
Path: .opencode/
|
|
95
|
+
Path: .opencode/plugins/agency-repository-skills.ts
|
|
91
96
|
The managed OpenCode workbase plugin needs synchronization.
|
|
92
97
|
Action: Run 'agency integration sync' to provide workbase access and expose writable-checkout skills in OpenCode.
|
|
93
98
|
|
|
@@ -99,7 +104,12 @@ OpenCode TUI config: missing
|
|
|
99
104
|
OpenCode /agency-debug: missing
|
|
100
105
|
Path: .opencode/tui/agency-debug.ts
|
|
101
106
|
The managed OpenCode TUI diagnostic companion needs synchronization.
|
|
102
|
-
Action: Run 'agency integration sync' to install /agency-debug
|
|
107
|
+
Action: Run 'agency integration sync' to install /agency-debug.
|
|
108
|
+
|
|
109
|
+
Pi workbase extension: missing
|
|
110
|
+
Path: .pi/extensions/agency-workbase.ts
|
|
111
|
+
The managed Pi workbase extension needs synchronization.
|
|
112
|
+
Action: Run 'agency integration sync' to provide workbase context and expose writable-checkout skills in Pi.`)
|
|
103
113
|
})
|
|
104
114
|
|
|
105
115
|
test("explicitly synchronizes integration files", async () => {
|
|
@@ -113,6 +123,7 @@ OpenCode /agency-debug: missing
|
|
|
113
123
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
114
124
|
{ name: "opencode-tui", state: "managed", changed: true },
|
|
115
125
|
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
126
|
+
{ name: "pi-extension", state: "managed", changed: true },
|
|
116
127
|
])
|
|
117
128
|
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
118
129
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(true)
|
|
@@ -124,7 +135,7 @@ OpenCode /agency-debug: missing
|
|
|
124
135
|
).toBe(false)
|
|
125
136
|
expect(
|
|
126
137
|
await Bun.file(
|
|
127
|
-
join(root, ".opencode/
|
|
138
|
+
join(root, ".opencode/plugins/agency-repository-skills.ts"),
|
|
128
139
|
).exists(),
|
|
129
140
|
).toBe(true)
|
|
130
141
|
expect(await Bun.file(join(root, ".opencode/tui.jsonc")).exists()).toBe(
|
|
@@ -133,6 +144,9 @@ OpenCode /agency-debug: missing
|
|
|
133
144
|
expect(
|
|
134
145
|
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).exists(),
|
|
135
146
|
).toBe(true)
|
|
147
|
+
expect(
|
|
148
|
+
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).exists(),
|
|
149
|
+
).toBe(true)
|
|
136
150
|
})
|
|
137
151
|
|
|
138
152
|
test("formats integration sync results for people", async () => {
|
|
@@ -151,7 +165,7 @@ OpenCode config: synced
|
|
|
151
165
|
Agency's managed OpenCode launch config is ready to load Agency instructions and provide whole-workbase read access.
|
|
152
166
|
|
|
153
167
|
OpenCode workbase plugin: synced
|
|
154
|
-
Path: .opencode/
|
|
168
|
+
Path: .opencode/plugins/agency-repository-skills.ts
|
|
155
169
|
Agency's managed OpenCode plugin provides whole-workbase access and exposes writable-checkout skills.
|
|
156
170
|
|
|
157
171
|
OpenCode TUI config: synced
|
|
@@ -160,6 +174,10 @@ OpenCode TUI config: synced
|
|
|
160
174
|
|
|
161
175
|
OpenCode /agency-debug: synced
|
|
162
176
|
Path: .opencode/tui/agency-debug.ts
|
|
163
|
-
Agency's managed OpenCode TUI diagnostic companion is current
|
|
177
|
+
Agency's managed OpenCode TUI diagnostic companion is current.
|
|
178
|
+
|
|
179
|
+
Pi workbase extension: synced
|
|
180
|
+
Path: .pi/extensions/agency-workbase.ts
|
|
181
|
+
Agency's managed Pi extension provides whole-workbase context and exposes writable-checkout skills.`)
|
|
164
182
|
})
|
|
165
183
|
})
|
|
@@ -18,6 +18,7 @@ interface IntegrationResult {
|
|
|
18
18
|
| "opencode-plugin"
|
|
19
19
|
| "opencode-tui"
|
|
20
20
|
| "opencode-tui-plugin"
|
|
21
|
+
| "pi-extension"
|
|
21
22
|
readonly path: string
|
|
22
23
|
readonly state: string
|
|
23
24
|
readonly diagnostic: string
|
|
@@ -32,6 +33,7 @@ const integrationNames = {
|
|
|
32
33
|
"opencode-plugin": "OpenCode workbase plugin",
|
|
33
34
|
"opencode-tui": "OpenCode TUI config",
|
|
34
35
|
"opencode-tui-plugin": "OpenCode /agency-debug",
|
|
36
|
+
"pi-extension": "Pi workbase extension",
|
|
35
37
|
} as const
|
|
36
38
|
|
|
37
39
|
const logHumanResult = (
|
|
@@ -89,10 +91,10 @@ export const help = `
|
|
|
89
91
|
Usage: agency integration <subcommand>
|
|
90
92
|
|
|
91
93
|
Inspect or explicitly synchronize managed agent integration files. OpenCode
|
|
92
|
-
launches load the managed instructions and project config at runtime.
|
|
93
|
-
|
|
94
|
-
the /agency-debug TUI diagnostic reports integration state
|
|
95
|
-
Agency write authority.
|
|
94
|
+
launches load the managed instructions and project config at runtime. Managed
|
|
95
|
+
OpenCode and Pi extensions provide whole-workbase context and writable-checkout
|
|
96
|
+
skills, and the /agency-debug TUI diagnostic reports OpenCode integration state
|
|
97
|
+
without changing Agency write authority.
|
|
96
98
|
|
|
97
99
|
Subcommands:
|
|
98
100
|
status Report file state, access diagnostics, and safe remediation
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
canUpdateManagedWorkbaseOpencodeTuiPlugin,
|
|
17
17
|
managedWorkbaseOpencodeTuiPlugin,
|
|
18
18
|
} from "../workbase/opencode-tui-plugin-file"
|
|
19
|
+
import {
|
|
20
|
+
canUpdateManagedWorkbasePiExtension,
|
|
21
|
+
managedWorkbasePiExtension,
|
|
22
|
+
} from "../workbase/pi-extension-file"
|
|
19
23
|
import { IntegrationService } from "./IntegrationService"
|
|
20
24
|
|
|
21
25
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -65,6 +69,7 @@ describe("IntegrationService", () => {
|
|
|
65
69
|
"missing",
|
|
66
70
|
"missing",
|
|
67
71
|
"missing",
|
|
72
|
+
"missing",
|
|
68
73
|
])
|
|
69
74
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
70
75
|
|
|
@@ -72,7 +77,7 @@ describe("IntegrationService", () => {
|
|
|
72
77
|
await write(root, ".opencode/opencode.jsonc", managedWorkbaseOpencode)
|
|
73
78
|
await write(
|
|
74
79
|
root,
|
|
75
|
-
".opencode/
|
|
80
|
+
".opencode/plugins/agency-repository-skills.ts",
|
|
76
81
|
managedWorkbaseOpencodePlugin,
|
|
77
82
|
)
|
|
78
83
|
await write(root, ".opencode/tui.jsonc", managedWorkbaseOpencodeTui)
|
|
@@ -81,12 +86,18 @@ describe("IntegrationService", () => {
|
|
|
81
86
|
".opencode/tui/agency-debug.ts",
|
|
82
87
|
managedWorkbaseOpencodeTuiPlugin,
|
|
83
88
|
)
|
|
89
|
+
await write(
|
|
90
|
+
root,
|
|
91
|
+
".pi/extensions/agency-workbase.ts",
|
|
92
|
+
managedWorkbasePiExtension,
|
|
93
|
+
)
|
|
84
94
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
85
95
|
"managed",
|
|
86
96
|
"managed",
|
|
87
97
|
"managed",
|
|
88
98
|
"managed",
|
|
89
99
|
"managed",
|
|
100
|
+
"managed",
|
|
90
101
|
])
|
|
91
102
|
})
|
|
92
103
|
|
|
@@ -104,6 +115,7 @@ describe("IntegrationService", () => {
|
|
|
104
115
|
"missing",
|
|
105
116
|
"missing",
|
|
106
117
|
"missing",
|
|
118
|
+
"missing",
|
|
107
119
|
])
|
|
108
120
|
})
|
|
109
121
|
|
|
@@ -126,6 +138,15 @@ describe("IntegrationService", () => {
|
|
|
126
138
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
127
139
|
"process.env.AGENCY_WRITABLE_CHECKOUT",
|
|
128
140
|
)
|
|
141
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
142
|
+
'import type { Plugin, PluginModule } from "@opencode-ai/plugin/v1"',
|
|
143
|
+
)
|
|
144
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
145
|
+
"export const AgencyPlugin = plugin",
|
|
146
|
+
)
|
|
147
|
+
expect(managedWorkbaseOpencodePlugin).toContain('id: "agency"')
|
|
148
|
+
expect(managedWorkbaseOpencodePlugin).toContain("setup,")
|
|
149
|
+
expect(managedWorkbaseOpencodePlugin).toContain("server: plugin")
|
|
129
150
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
130
151
|
'["agency", "context", ".", "--compact", "--json"]',
|
|
131
152
|
)
|
|
@@ -199,6 +220,131 @@ describe("IntegrationService", () => {
|
|
|
199
220
|
).toBe(false)
|
|
200
221
|
})
|
|
201
222
|
|
|
223
|
+
test("generates a Pi extension with workbase context and checkout skills", async () => {
|
|
224
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
225
|
+
'import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"',
|
|
226
|
+
)
|
|
227
|
+
expect(managedWorkbasePiExtension).toContain('pi.on("resources_discover"')
|
|
228
|
+
expect(managedWorkbasePiExtension).toContain('pi.on("before_agent_start"')
|
|
229
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
230
|
+
'join(checkout, ".agents", "skills")',
|
|
231
|
+
)
|
|
232
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
233
|
+
'join(checkout, CONFIG_DIR_NAME, "skills")',
|
|
234
|
+
)
|
|
235
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
236
|
+
"The complete Agency workbase is available",
|
|
237
|
+
)
|
|
238
|
+
expect(managedWorkbasePiExtension).toContain(
|
|
239
|
+
"Agency context remains the authority for writes",
|
|
240
|
+
)
|
|
241
|
+
expect(
|
|
242
|
+
canUpdateManagedWorkbasePiExtension(managedWorkbasePiExtension),
|
|
243
|
+
).toBe(true)
|
|
244
|
+
expect(
|
|
245
|
+
canUpdateManagedWorkbasePiExtension(
|
|
246
|
+
managedWorkbasePiExtension.replace("resources_discover", "discover"),
|
|
247
|
+
),
|
|
248
|
+
).toBe(false)
|
|
249
|
+
|
|
250
|
+
const taskDirectory = join(root, "tasks/example")
|
|
251
|
+
const checkoutPath = join(taskDirectory, "code/agency")
|
|
252
|
+
await write(root, ".agency/AGENTS.md", "# Managed Agency instructions\n")
|
|
253
|
+
await write(
|
|
254
|
+
root,
|
|
255
|
+
"tasks/example/TASK.md",
|
|
256
|
+
"---\nrepo: agency\nstatus: working\n---\n",
|
|
257
|
+
)
|
|
258
|
+
await write(
|
|
259
|
+
root,
|
|
260
|
+
"tasks/example/code/agency/.agents/skills/example/SKILL.md",
|
|
261
|
+
"---\nname: example\ndescription: Example.\n---\n",
|
|
262
|
+
)
|
|
263
|
+
await write(
|
|
264
|
+
root,
|
|
265
|
+
"tasks/example/code/agency/.pi/skills/pi-example/SKILL.md",
|
|
266
|
+
"---\nname: pi-example\ndescription: Pi example.\n---\n",
|
|
267
|
+
)
|
|
268
|
+
const path = join(root, ".pi/extensions/agency-workbase.ts")
|
|
269
|
+
await write(
|
|
270
|
+
root,
|
|
271
|
+
".pi/extensions/agency-workbase.ts",
|
|
272
|
+
managedWorkbasePiExtension.replace(
|
|
273
|
+
'import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"',
|
|
274
|
+
'const CONFIG_DIR_NAME = ".pi"\ntype ExtensionAPI = any',
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
const contextResponse = JSON.stringify({
|
|
279
|
+
version: 1,
|
|
280
|
+
ok: true,
|
|
281
|
+
result: {
|
|
282
|
+
workbase: { root },
|
|
283
|
+
target: {
|
|
284
|
+
kind: "task",
|
|
285
|
+
taskId: "example",
|
|
286
|
+
path: join(taskDirectory, "TASK.md"),
|
|
287
|
+
},
|
|
288
|
+
authority: {
|
|
289
|
+
mode: "execution",
|
|
290
|
+
writable: { checkoutPath },
|
|
291
|
+
},
|
|
292
|
+
documents: { task: { data: { status: "working" } } },
|
|
293
|
+
validation: { valid: true, warnings: [] },
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
const handlers = new Map<string, (...args: any[]) => any>()
|
|
297
|
+
const pi = {
|
|
298
|
+
exec: async () => ({ code: 0, stdout: contextResponse, stderr: "" }),
|
|
299
|
+
on: (name: string, handler: (...args: any[]) => any) => {
|
|
300
|
+
handlers.set(name, handler)
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
const previousTarget = process.env.AGENCY_TARGET
|
|
304
|
+
const previousWorkbase = process.env.AGENCY_WORKBASE
|
|
305
|
+
const previousCheckout = process.env.AGENCY_WRITABLE_CHECKOUT
|
|
306
|
+
delete process.env.AGENCY_WORKBASE
|
|
307
|
+
delete process.env.AGENCY_WRITABLE_CHECKOUT
|
|
308
|
+
process.env.AGENCY_TARGET = "execution-unit:task/example"
|
|
309
|
+
try {
|
|
310
|
+
const generated = await import(
|
|
311
|
+
`${pathToFileURL(path).href}?pi-extension=${Date.now()}`
|
|
312
|
+
)
|
|
313
|
+
generated.default(pi)
|
|
314
|
+
const resources = await handlers.get("resources_discover")?.({
|
|
315
|
+
cwd: taskDirectory,
|
|
316
|
+
reason: "startup",
|
|
317
|
+
})
|
|
318
|
+
expect(resources.skillPaths).toEqual([
|
|
319
|
+
join(checkoutPath, ".agents/skills"),
|
|
320
|
+
join(checkoutPath, ".pi/skills"),
|
|
321
|
+
])
|
|
322
|
+
|
|
323
|
+
const prompt = await handlers.get("before_agent_start")?.(
|
|
324
|
+
{ systemPrompt: "Base prompt" },
|
|
325
|
+
{ cwd: taskDirectory },
|
|
326
|
+
)
|
|
327
|
+
expect(prompt.systemPrompt).toContain("# Managed Agency instructions")
|
|
328
|
+
expect(prompt.systemPrompt).toContain(
|
|
329
|
+
`The complete Agency workbase is available at ${root}`,
|
|
330
|
+
)
|
|
331
|
+
expect(prompt.systemPrompt).toContain(
|
|
332
|
+
"active worker for execution-unit:task/example",
|
|
333
|
+
)
|
|
334
|
+
expect(prompt.systemPrompt).toContain(
|
|
335
|
+
`${checkoutPath} as the default implementation directory`,
|
|
336
|
+
)
|
|
337
|
+
} finally {
|
|
338
|
+
if (previousTarget === undefined) delete process.env.AGENCY_TARGET
|
|
339
|
+
else process.env.AGENCY_TARGET = previousTarget
|
|
340
|
+
if (previousWorkbase === undefined) delete process.env.AGENCY_WORKBASE
|
|
341
|
+
else process.env.AGENCY_WORKBASE = previousWorkbase
|
|
342
|
+
if (previousCheckout === undefined)
|
|
343
|
+
delete process.env.AGENCY_WRITABLE_CHECKOUT
|
|
344
|
+
else process.env.AGENCY_WRITABLE_CHECKOUT = previousCheckout
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
202
348
|
const testWorkerIdentity = async ({
|
|
203
349
|
target,
|
|
204
350
|
launchTarget,
|
|
@@ -276,7 +422,12 @@ describe("IntegrationService", () => {
|
|
|
276
422
|
task: "example",
|
|
277
423
|
phase,
|
|
278
424
|
})
|
|
279
|
-
|
|
425
|
+
expect(generated.default).toMatchObject({
|
|
426
|
+
id: "agency",
|
|
427
|
+
setup: expect.any(Function),
|
|
428
|
+
server: generated.AgencyPlugin,
|
|
429
|
+
})
|
|
430
|
+
const hooks = await generated.AgencyPlugin({ directory: root } as never)
|
|
280
431
|
await hooks["chat.message"]!(
|
|
281
432
|
{ sessionID: "worker-session" } as never,
|
|
282
433
|
{
|
|
@@ -351,6 +502,52 @@ describe("IntegrationService", () => {
|
|
|
351
502
|
phase: "build",
|
|
352
503
|
}))
|
|
353
504
|
|
|
505
|
+
test("registers the workbase reference through the OpenCode V2 API", async () => {
|
|
506
|
+
const path = join(root, ".opencode/plugins/agency-repository-skills.ts")
|
|
507
|
+
await write(
|
|
508
|
+
root,
|
|
509
|
+
".opencode/plugins/agency-repository-skills.ts",
|
|
510
|
+
managedWorkbaseOpencodePlugin,
|
|
511
|
+
)
|
|
512
|
+
const generated = await import(`${pathToFileURL(path).href}?v2-setup`)
|
|
513
|
+
let reference:
|
|
514
|
+
| {
|
|
515
|
+
name: string
|
|
516
|
+
source: { type: string; path: string; description: string }
|
|
517
|
+
}
|
|
518
|
+
| undefined
|
|
519
|
+
|
|
520
|
+
await generated.default.setup({
|
|
521
|
+
reference: {
|
|
522
|
+
transform: async (callback: (references: unknown) => void) =>
|
|
523
|
+
callback({
|
|
524
|
+
list: () => [],
|
|
525
|
+
add: (
|
|
526
|
+
name: string,
|
|
527
|
+
source: {
|
|
528
|
+
type: string
|
|
529
|
+
path: string
|
|
530
|
+
description: string
|
|
531
|
+
},
|
|
532
|
+
) => {
|
|
533
|
+
reference = { name, source }
|
|
534
|
+
},
|
|
535
|
+
}),
|
|
536
|
+
},
|
|
537
|
+
skill: { transform: async () => {} },
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
expect(reference).toEqual({
|
|
541
|
+
name: "workbase",
|
|
542
|
+
source: {
|
|
543
|
+
type: "local",
|
|
544
|
+
path: root,
|
|
545
|
+
description:
|
|
546
|
+
"Complete Agency workbase context; write authority still comes only from agency context",
|
|
547
|
+
},
|
|
548
|
+
})
|
|
549
|
+
})
|
|
550
|
+
|
|
354
551
|
test("registers a TUI-only /agency-debug diagnostic", async () => {
|
|
355
552
|
const config = JSON.parse(managedBody(managedWorkbaseOpencodeTui))
|
|
356
553
|
expect(config).toEqual({
|
|
@@ -673,6 +870,7 @@ describe("IntegrationService", () => {
|
|
|
673
870
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
674
871
|
{ name: "opencode-tui", state: "managed", changed: true },
|
|
675
872
|
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
873
|
+
{ name: "pi-extension", state: "managed", changed: true },
|
|
676
874
|
])
|
|
677
875
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
678
876
|
customRootAgents,
|
|
@@ -688,7 +886,7 @@ describe("IntegrationService", () => {
|
|
|
688
886
|
).toBe(false)
|
|
689
887
|
expect(
|
|
690
888
|
await Bun.file(
|
|
691
|
-
join(root, ".opencode/
|
|
889
|
+
join(root, ".opencode/plugins/agency-repository-skills.ts"),
|
|
692
890
|
).text(),
|
|
693
891
|
).toBe(managedWorkbaseOpencodePlugin)
|
|
694
892
|
expect(await Bun.file(join(root, ".opencode/tui.jsonc")).text()).toBe(
|
|
@@ -697,6 +895,9 @@ describe("IntegrationService", () => {
|
|
|
697
895
|
expect(
|
|
698
896
|
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
|
|
699
897
|
).toBe(managedWorkbaseOpencodeTuiPlugin)
|
|
898
|
+
expect(
|
|
899
|
+
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).text(),
|
|
900
|
+
).toBe(managedWorkbasePiExtension)
|
|
700
901
|
|
|
701
902
|
await unlink(join(root, ".agency/AGENTS.md"))
|
|
702
903
|
const second = await sync(root)
|
|
@@ -761,6 +962,50 @@ describe("IntegrationService", () => {
|
|
|
761
962
|
})
|
|
762
963
|
})
|
|
763
964
|
|
|
965
|
+
test("migrates a checksum-valid plugin from the legacy singular path", async () => {
|
|
966
|
+
const legacyPath = join(
|
|
967
|
+
root,
|
|
968
|
+
".opencode/plugin/agency-repository-skills.ts",
|
|
969
|
+
)
|
|
970
|
+
const pluginPath = join(
|
|
971
|
+
root,
|
|
972
|
+
".opencode/plugins/agency-repository-skills.ts",
|
|
973
|
+
)
|
|
974
|
+
await write(
|
|
975
|
+
root,
|
|
976
|
+
".opencode/plugin/agency-repository-skills.ts",
|
|
977
|
+
managedWorkbaseOpencodePlugin,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
const result = await sync(root)
|
|
981
|
+
|
|
982
|
+
expect(result.files[2]).toMatchObject({
|
|
983
|
+
name: "opencode-plugin",
|
|
984
|
+
path: pluginPath,
|
|
985
|
+
state: "managed",
|
|
986
|
+
changed: true,
|
|
987
|
+
})
|
|
988
|
+
expect(await Bun.file(pluginPath).text()).toBe(
|
|
989
|
+
managedWorkbaseOpencodePlugin,
|
|
990
|
+
)
|
|
991
|
+
expect(await Bun.file(legacyPath).exists()).toBe(false)
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
test("preserves a user-owned Pi extension", async () => {
|
|
995
|
+
const custom = "export default function customPiExtension() {}\n"
|
|
996
|
+
await write(root, ".pi/extensions/agency-workbase.ts", custom)
|
|
997
|
+
|
|
998
|
+
const result = await sync(root)
|
|
999
|
+
expect(result.files[5]).toMatchObject({
|
|
1000
|
+
name: "pi-extension",
|
|
1001
|
+
state: "customized",
|
|
1002
|
+
changed: false,
|
|
1003
|
+
})
|
|
1004
|
+
expect(
|
|
1005
|
+
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).text(),
|
|
1006
|
+
).toBe(custom)
|
|
1007
|
+
})
|
|
1008
|
+
|
|
764
1009
|
test("preserves user-owned TUI config and diagnostic plugin", async () => {
|
|
765
1010
|
const customConfig = '{"theme":"custom"}\n'
|
|
766
1011
|
const customPlugin =
|
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
canUpdateManagedWorkbaseOpencodeTuiPlugin,
|
|
24
24
|
managedWorkbaseOpencodeTuiPlugin,
|
|
25
25
|
} from "../workbase/opencode-tui-plugin-file"
|
|
26
|
+
import {
|
|
27
|
+
canUpdateManagedWorkbasePiExtension,
|
|
28
|
+
managedWorkbasePiExtension,
|
|
29
|
+
} from "../workbase/pi-extension-file"
|
|
26
30
|
|
|
27
31
|
type IntegrationFileState = "managed" | "customized" | "missing" | "drifted"
|
|
28
32
|
|
|
@@ -33,6 +37,7 @@ interface IntegrationFileStatus {
|
|
|
33
37
|
| "opencode-plugin"
|
|
34
38
|
| "opencode-tui"
|
|
35
39
|
| "opencode-tui-plugin"
|
|
40
|
+
| "pi-extension"
|
|
36
41
|
readonly path: string
|
|
37
42
|
readonly state: IntegrationFileState
|
|
38
43
|
readonly diagnostic: string
|
|
@@ -131,6 +136,26 @@ const describe = (
|
|
|
131
136
|
"Run 'agency integration sync' to install /agency-debug.",
|
|
132
137
|
}
|
|
133
138
|
}
|
|
139
|
+
if (name === "pi-extension") {
|
|
140
|
+
return state === "managed"
|
|
141
|
+
? {
|
|
142
|
+
diagnostic:
|
|
143
|
+
"Agency's managed Pi extension provides whole-workbase context and exposes writable-checkout skills.",
|
|
144
|
+
remediation: null,
|
|
145
|
+
}
|
|
146
|
+
: state === "customized"
|
|
147
|
+
? {
|
|
148
|
+
diagnostic:
|
|
149
|
+
"A user-owned Pi workbase extension is present and was preserved.",
|
|
150
|
+
remediation: null,
|
|
151
|
+
}
|
|
152
|
+
: {
|
|
153
|
+
diagnostic:
|
|
154
|
+
"The managed Pi workbase extension needs synchronization.",
|
|
155
|
+
remediation:
|
|
156
|
+
"Run 'agency integration sync' to provide workbase context and expose writable-checkout skills in Pi.",
|
|
157
|
+
}
|
|
158
|
+
}
|
|
134
159
|
|
|
135
160
|
return state === "missing" || state === "drifted"
|
|
136
161
|
? {
|
|
@@ -179,17 +204,23 @@ const inspect = (root: string) =>
|
|
|
179
204
|
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
180
205
|
const tuiPath = join(opencodeDirectory, "tui.jsonc")
|
|
181
206
|
const tuiJsonPath = join(opencodeDirectory, "tui.json")
|
|
182
|
-
const
|
|
207
|
+
const legacyPluginPath = join(
|
|
183
208
|
opencodeDirectory,
|
|
184
209
|
"plugin",
|
|
185
210
|
"agency-repository-skills.ts",
|
|
186
211
|
)
|
|
187
|
-
const
|
|
212
|
+
const pluginPath = join(
|
|
188
213
|
opencodeDirectory,
|
|
189
214
|
"plugins",
|
|
190
215
|
"agency-repository-skills.ts",
|
|
191
216
|
)
|
|
192
217
|
const tuiPluginPath = join(opencodeDirectory, "tui", "agency-debug.ts")
|
|
218
|
+
const piExtensionPath = join(
|
|
219
|
+
root,
|
|
220
|
+
".pi",
|
|
221
|
+
"extensions",
|
|
222
|
+
"agency-workbase.ts",
|
|
223
|
+
)
|
|
193
224
|
const files: IntegrationFileStatus[] = []
|
|
194
225
|
|
|
195
226
|
files.push(
|
|
@@ -229,11 +260,6 @@ const inspect = (root: string) =>
|
|
|
229
260
|
|
|
230
261
|
if ((yield* fs.readSymlinkTarget(pluginPath)) !== null) {
|
|
231
262
|
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
263
|
} else if (yield* fs.exists(pluginPath)) {
|
|
238
264
|
files.push(
|
|
239
265
|
classify(
|
|
@@ -244,6 +270,20 @@ const inspect = (root: string) =>
|
|
|
244
270
|
canUpdateManagedWorkbaseOpencodePlugin,
|
|
245
271
|
),
|
|
246
272
|
)
|
|
273
|
+
} else if (
|
|
274
|
+
(yield* fs.readSymlinkTarget(legacyPluginPath)) !== null ||
|
|
275
|
+
(yield* fs.exists(legacyPluginPath))
|
|
276
|
+
) {
|
|
277
|
+
const legacyContent =
|
|
278
|
+
(yield* fs.readSymlinkTarget(legacyPluginPath)) === null
|
|
279
|
+
? yield* fs.readFile(legacyPluginPath)
|
|
280
|
+
: null
|
|
281
|
+
files.push(
|
|
282
|
+
legacyContent !== null &&
|
|
283
|
+
canUpdateManagedWorkbaseOpencodePlugin(legacyContent)
|
|
284
|
+
? fileStatus("opencode-plugin", pluginPath, "missing")
|
|
285
|
+
: fileStatus("opencode-plugin", legacyPluginPath, "customized"),
|
|
286
|
+
)
|
|
247
287
|
} else {
|
|
248
288
|
files.push(fileStatus("opencode-plugin", pluginPath, "missing"))
|
|
249
289
|
}
|
|
@@ -285,6 +325,22 @@ const inspect = (root: string) =>
|
|
|
285
325
|
files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "missing"))
|
|
286
326
|
}
|
|
287
327
|
|
|
328
|
+
if ((yield* fs.readSymlinkTarget(piExtensionPath)) !== null) {
|
|
329
|
+
files.push(fileStatus("pi-extension", piExtensionPath, "customized"))
|
|
330
|
+
} else if (yield* fs.exists(piExtensionPath)) {
|
|
331
|
+
files.push(
|
|
332
|
+
classify(
|
|
333
|
+
"pi-extension",
|
|
334
|
+
piExtensionPath,
|
|
335
|
+
yield* fs.readFile(piExtensionPath),
|
|
336
|
+
managedWorkbasePiExtension,
|
|
337
|
+
canUpdateManagedWorkbasePiExtension,
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
} else {
|
|
341
|
+
files.push(fileStatus("pi-extension", piExtensionPath, "missing"))
|
|
342
|
+
}
|
|
343
|
+
|
|
288
344
|
return files
|
|
289
345
|
})
|
|
290
346
|
|
|
@@ -319,6 +375,23 @@ const canRemoveLegacyOpencodeCommand = (root: string) =>
|
|
|
319
375
|
return createHash("sha256").update(canonical).digest("hex") === match[1]
|
|
320
376
|
})
|
|
321
377
|
|
|
378
|
+
const canRemoveLegacyOpencodePlugin = (root: string) =>
|
|
379
|
+
Effect.gen(function* () {
|
|
380
|
+
const fs = yield* FileSystemService
|
|
381
|
+
const path = join(
|
|
382
|
+
root,
|
|
383
|
+
".opencode",
|
|
384
|
+
"plugin",
|
|
385
|
+
"agency-repository-skills.ts",
|
|
386
|
+
)
|
|
387
|
+
if (
|
|
388
|
+
(yield* fs.readSymlinkTarget(path)) !== null ||
|
|
389
|
+
!(yield* fs.exists(path))
|
|
390
|
+
)
|
|
391
|
+
return false
|
|
392
|
+
return canUpdateManagedWorkbaseOpencodePlugin(yield* fs.readFile(path))
|
|
393
|
+
})
|
|
394
|
+
|
|
322
395
|
export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
323
396
|
"IntegrationService",
|
|
324
397
|
{
|
|
@@ -343,6 +416,8 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
343
416
|
) && (yield* canRemoveLegacyAgents(root))
|
|
344
417
|
const removeLegacyOpencodeCommand =
|
|
345
418
|
yield* canRemoveLegacyOpencodeCommand(root)
|
|
419
|
+
const removeLegacyOpencodePlugin =
|
|
420
|
+
yield* canRemoveLegacyOpencodePlugin(root)
|
|
346
421
|
const files: IntegrationSyncFile[] = []
|
|
347
422
|
|
|
348
423
|
for (const status of statuses) {
|
|
@@ -356,17 +431,20 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
356
431
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
357
432
|
yield* fs.writeFile(status.path, managedWorkbaseOpencode)
|
|
358
433
|
} else if (status.name === "opencode-plugin") {
|
|
359
|
-
yield* fs.createDirectory(join(root, ".opencode", "
|
|
434
|
+
yield* fs.createDirectory(join(root, ".opencode", "plugins"))
|
|
360
435
|
yield* fs.writeFile(status.path, managedWorkbaseOpencodePlugin)
|
|
361
436
|
} else if (status.name === "opencode-tui") {
|
|
362
437
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
363
438
|
yield* fs.writeFile(status.path, managedWorkbaseOpencodeTui)
|
|
364
|
-
} else {
|
|
439
|
+
} else if (status.name === "opencode-tui-plugin") {
|
|
365
440
|
yield* fs.createDirectory(join(root, ".opencode", "tui"))
|
|
366
441
|
yield* fs.writeFile(
|
|
367
442
|
status.path,
|
|
368
443
|
managedWorkbaseOpencodeTuiPlugin,
|
|
369
444
|
)
|
|
445
|
+
} else {
|
|
446
|
+
yield* fs.createDirectory(join(root, ".pi", "extensions"))
|
|
447
|
+
yield* fs.writeFile(status.path, managedWorkbasePiExtension)
|
|
370
448
|
}
|
|
371
449
|
}
|
|
372
450
|
files.push({
|
|
@@ -376,11 +454,19 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
376
454
|
needsWrite ? "managed" : status.state,
|
|
377
455
|
),
|
|
378
456
|
changed:
|
|
379
|
-
needsWrite ||
|
|
457
|
+
needsWrite ||
|
|
458
|
+
(status.name === "agents" && removeLegacyAgents) ||
|
|
459
|
+
(status.name === "opencode-plugin" &&
|
|
460
|
+
removeLegacyOpencodePlugin),
|
|
380
461
|
})
|
|
381
462
|
}
|
|
382
463
|
|
|
383
464
|
if (removeLegacyAgents) yield* fs.deleteFile(join(root, "AGENTS.md"))
|
|
465
|
+
if (removeLegacyOpencodePlugin) {
|
|
466
|
+
yield* fs.deleteFile(
|
|
467
|
+
join(root, ".opencode", "plugin", "agency-repository-skills.ts"),
|
|
468
|
+
)
|
|
469
|
+
}
|
|
384
470
|
if (removeLegacyOpencodeCommand) {
|
|
385
471
|
yield* fs.deleteFile(
|
|
386
472
|
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) =>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
const managedHeaderPattern =
|
|
4
|
+
/^\/\/ agency-managed: sha256=([a-f0-9]{64})\r?\n\r?\n/
|
|
5
|
+
|
|
6
|
+
const checksum = (content: string) =>
|
|
7
|
+
createHash("sha256").update(content).digest("hex")
|
|
8
|
+
|
|
9
|
+
const body = `import { existsSync, readFileSync } from "node:fs"
|
|
10
|
+
import { dirname, join, resolve } from "node:path"
|
|
11
|
+
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
12
|
+
|
|
13
|
+
type AgencyContext = {
|
|
14
|
+
root?: string
|
|
15
|
+
checkout?: string
|
|
16
|
+
target?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const contextTarget = (result: Record<string, any>): string | undefined => {
|
|
20
|
+
const target = result.target
|
|
21
|
+
if (target?.kind === "epic") return \`epic:\${target.epicId}\`
|
|
22
|
+
if (target?.kind === "phase") {
|
|
23
|
+
return \`execution-unit:phase/\${target.taskId}/\${target.phaseId}\`
|
|
24
|
+
}
|
|
25
|
+
if (target?.kind === "task") {
|
|
26
|
+
return result.authority?.mode === "execution"
|
|
27
|
+
? \`execution-unit:task/\${target.taskId}\`
|
|
28
|
+
: \`task:\${target.taskId}\`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const discoverWorkbase = (directory: string) => {
|
|
33
|
+
let current = directory
|
|
34
|
+
while (true) {
|
|
35
|
+
if (existsSync(join(current, "agency.json"))) return current
|
|
36
|
+
const parent = dirname(current)
|
|
37
|
+
if (parent === current) return
|
|
38
|
+
current = parent
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const discoverCheckout = (directory: string, root: string | undefined) => {
|
|
43
|
+
if (!root) return
|
|
44
|
+
let current = directory
|
|
45
|
+
while (current.startsWith(root)) {
|
|
46
|
+
for (const name of ["PHASE.md", "TASK.md"]) {
|
|
47
|
+
const document = join(current, name)
|
|
48
|
+
if (!existsSync(document)) continue
|
|
49
|
+
const repo = readFileSync(document, "utf8").match(/^repo:\\s*([^\\s]+)\\s*$/m)?.[1]
|
|
50
|
+
if (!repo) return
|
|
51
|
+
const checkout = join(current, "code", repo.replace(/^['\"]|['\"]$/g, ""))
|
|
52
|
+
return existsSync(checkout) ? checkout : undefined
|
|
53
|
+
}
|
|
54
|
+
if (current === root) return
|
|
55
|
+
current = dirname(current)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const agencyContext = async (
|
|
60
|
+
pi: ExtensionAPI,
|
|
61
|
+
directory: string,
|
|
62
|
+
): Promise<AgencyContext | undefined> => {
|
|
63
|
+
const task = process.env.AGENCY_TASK_ID
|
|
64
|
+
const phase = process.env.AGENCY_PHASE_ID
|
|
65
|
+
const args = task
|
|
66
|
+
? ["context", "--task", task, ...(phase ? ["--phase", phase] : []), "--compact", "--json"]
|
|
67
|
+
: ["context", ".", "--compact", "--json"]
|
|
68
|
+
const command = await pi.exec("agency", args, { timeout: 5000 })
|
|
69
|
+
if (command.code !== 0) return
|
|
70
|
+
const envelope = JSON.parse(command.stdout)
|
|
71
|
+
if (envelope.ok !== true) return
|
|
72
|
+
const result = envelope.result ?? {}
|
|
73
|
+
const target = contextTarget(result)
|
|
74
|
+
const document = result.target?.path
|
|
75
|
+
const status = result.documents?.phase?.data?.status ?? result.documents?.task?.data?.status
|
|
76
|
+
if (
|
|
77
|
+
result.validation?.valid !== true ||
|
|
78
|
+
!target ||
|
|
79
|
+
!document ||
|
|
80
|
+
dirname(document) !== resolve(directory) ||
|
|
81
|
+
(target.startsWith("execution-unit:") &&
|
|
82
|
+
(!result.authority?.writable?.checkoutPath || status !== "working"))
|
|
83
|
+
) return
|
|
84
|
+
return {
|
|
85
|
+
root: result.workbase?.root,
|
|
86
|
+
checkout: result.authority?.writable?.checkoutPath,
|
|
87
|
+
target,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const extension = (pi: ExtensionAPI) => {
|
|
92
|
+
const contexts = new Map<string, Promise<AgencyContext | undefined>>()
|
|
93
|
+
const runtimeContext = (directory: string) => {
|
|
94
|
+
let context = contexts.get(directory)
|
|
95
|
+
if (!context) {
|
|
96
|
+
context = agencyContext(pi, directory).catch(() => undefined)
|
|
97
|
+
contexts.set(directory, context)
|
|
98
|
+
}
|
|
99
|
+
return context
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pi.on("resources_discover", async (event) => {
|
|
103
|
+
const context = await runtimeContext(event.cwd)
|
|
104
|
+
const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(event.cwd)
|
|
105
|
+
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(event.cwd, root)
|
|
106
|
+
if (!checkout) return
|
|
107
|
+
|
|
108
|
+
const skillPaths = [
|
|
109
|
+
join(checkout, ".claude", "skills"),
|
|
110
|
+
join(checkout, ".agents", "skills"),
|
|
111
|
+
join(checkout, ".opencode", "skill"),
|
|
112
|
+
join(checkout, ".opencode", "skills"),
|
|
113
|
+
join(checkout, CONFIG_DIR_NAME, "skills"),
|
|
114
|
+
].filter(existsSync)
|
|
115
|
+
if (skillPaths.length === 0) return
|
|
116
|
+
return { skillPaths: [...new Set(skillPaths)] }
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
120
|
+
const context = await runtimeContext(ctx.cwd)
|
|
121
|
+
const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(ctx.cwd)
|
|
122
|
+
if (!root) return
|
|
123
|
+
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(ctx.cwd, root)
|
|
124
|
+
const instructionsPath = join(root, ".agency", "AGENTS.md")
|
|
125
|
+
const instructions = existsSync(instructionsPath)
|
|
126
|
+
? readFileSync(instructionsPath, "utf8").trim()
|
|
127
|
+
: undefined
|
|
128
|
+
const activeTarget = process.env.AGENCY_TARGET
|
|
129
|
+
const worker = activeTarget && context?.target === activeTarget
|
|
130
|
+
? \`Agency verified this Pi session as the active worker for \${activeTarget}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`
|
|
131
|
+
: undefined
|
|
132
|
+
const access = \`The complete Agency workbase is available at \${root}. Use absolute paths under that root when workbase context is needed. Agency context remains the authority for writes.\`
|
|
133
|
+
const implementation = checkout
|
|
134
|
+
? \`Pi remains rooted in the task or phase directory for Agency instructions and context. Treat \${checkout} as the default implementation directory for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.\`
|
|
135
|
+
: undefined
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
systemPrompt: [event.systemPrompt, instructions, access, worker, implementation]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join("\\n\\n"),
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default extension
|
|
146
|
+
`
|
|
147
|
+
|
|
148
|
+
const renderManagedWorkbasePiExtension = (content: string) =>
|
|
149
|
+
`// agency-managed: sha256=${checksum(content)}\n\n${content}`
|
|
150
|
+
|
|
151
|
+
export const managedWorkbasePiExtension = renderManagedWorkbasePiExtension(body)
|
|
152
|
+
|
|
153
|
+
export const canUpdateManagedWorkbasePiExtension = (content: string) => {
|
|
154
|
+
const match = content.match(managedHeaderPattern)
|
|
155
|
+
if (!match?.[1]) return false
|
|
156
|
+
|
|
157
|
+
return checksum(content.slice(match[0].length)) === match[1]
|
|
158
|
+
}
|