@markjaquith/agency 2.67.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 +24 -9
- package/package.json +1 -1
- package/src/cli.test.ts +2 -0
- package/src/commands/init.test.ts +6 -0
- package/src/commands/integration.test.ts +20 -2
- package/src/commands/integration.ts +6 -4
- package/src/services/IntegrationService.test.ts +156 -0
- package/src/services/IntegrationService.ts +51 -1
- package/src/workbase/pi-extension-file.ts +158 -0
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ workbase/
|
|
|
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
|
|
|
@@ -126,6 +128,16 @@ The plugin grants whole-workbase access dynamically, while the portable
|
|
|
126
128
|
reference advertises that context to agents. Bash and Agency operations must
|
|
127
129
|
still follow the write authority reported by `agency context`.
|
|
128
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
|
+
|
|
129
141
|
Repository aliases, the version-control backend, and canonical fetch remotes are
|
|
130
142
|
declared in tracked `agency.json`; local clones and symlinks remain ignored under
|
|
131
143
|
`repos/{alias}`. A declaration contains no local path, symlink target, checkout,
|
|
@@ -409,11 +421,14 @@ working directory so the workbase `AGENTS.md` and managed OpenCode config are
|
|
|
409
421
|
discovered normally.
|
|
410
422
|
Agency's managed OpenCode plugin grants the active workbase external-directory
|
|
411
423
|
access and adds existing checkout-local `.claude/skills`, `.agents/skills`, and
|
|
412
|
-
`.opencode/{skill,skills}` directories to `skills.paths`.
|
|
413
|
-
|
|
414
|
-
|
|
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
|
|
415
430
|
task root has no single checkout, so launch from its phase directory when using
|
|
416
|
-
plain OpenCode. Other checkout-local configuration is not composed.
|
|
431
|
+
plain OpenCode or Pi. Other checkout-local configuration is not composed.
|
|
417
432
|
`--print-command` prints the exact cwd and argv plus non-secret environment keys
|
|
418
433
|
without launching the runner.
|
|
419
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
|
|
|
@@ -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
|
})
|
|
@@ -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)
|
|
@@ -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 () => {
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -208,6 +220,131 @@ describe("IntegrationService", () => {
|
|
|
208
220
|
).toBe(false)
|
|
209
221
|
})
|
|
210
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
|
+
|
|
211
348
|
const testWorkerIdentity = async ({
|
|
212
349
|
target,
|
|
213
350
|
launchTarget,
|
|
@@ -733,6 +870,7 @@ describe("IntegrationService", () => {
|
|
|
733
870
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
734
871
|
{ name: "opencode-tui", state: "managed", changed: true },
|
|
735
872
|
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
873
|
+
{ name: "pi-extension", state: "managed", changed: true },
|
|
736
874
|
])
|
|
737
875
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
738
876
|
customRootAgents,
|
|
@@ -757,6 +895,9 @@ describe("IntegrationService", () => {
|
|
|
757
895
|
expect(
|
|
758
896
|
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
|
|
759
897
|
).toBe(managedWorkbaseOpencodeTuiPlugin)
|
|
898
|
+
expect(
|
|
899
|
+
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).text(),
|
|
900
|
+
).toBe(managedWorkbasePiExtension)
|
|
760
901
|
|
|
761
902
|
await unlink(join(root, ".agency/AGENTS.md"))
|
|
762
903
|
const second = await sync(root)
|
|
@@ -850,6 +991,21 @@ describe("IntegrationService", () => {
|
|
|
850
991
|
expect(await Bun.file(legacyPath).exists()).toBe(false)
|
|
851
992
|
})
|
|
852
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
|
+
|
|
853
1009
|
test("preserves user-owned TUI config and diagnostic plugin", async () => {
|
|
854
1010
|
const customConfig = '{"theme":"custom"}\n'
|
|
855
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
|
? {
|
|
@@ -190,6 +215,12 @@ const inspect = (root: string) =>
|
|
|
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(
|
|
@@ -294,6 +325,22 @@ const inspect = (root: string) =>
|
|
|
294
325
|
files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "missing"))
|
|
295
326
|
}
|
|
296
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
|
+
|
|
297
344
|
return files
|
|
298
345
|
})
|
|
299
346
|
|
|
@@ -389,12 +436,15 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
389
436
|
} else if (status.name === "opencode-tui") {
|
|
390
437
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
391
438
|
yield* fs.writeFile(status.path, managedWorkbaseOpencodeTui)
|
|
392
|
-
} else {
|
|
439
|
+
} else if (status.name === "opencode-tui-plugin") {
|
|
393
440
|
yield* fs.createDirectory(join(root, ".opencode", "tui"))
|
|
394
441
|
yield* fs.writeFile(
|
|
395
442
|
status.path,
|
|
396
443
|
managedWorkbaseOpencodeTuiPlugin,
|
|
397
444
|
)
|
|
445
|
+
} else {
|
|
446
|
+
yield* fs.createDirectory(join(root, ".pi", "extensions"))
|
|
447
|
+
yield* fs.writeFile(status.path, managedWorkbasePiExtension)
|
|
398
448
|
}
|
|
399
449
|
}
|
|
400
450
|
files.push({
|
|
@@ -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
|
+
}
|