@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 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` and
87
- `.opencode/opencode.jsonc`, `.opencode/tui.jsonc`, and their managed plugin
88
- files, then `agency integration sync` to create missing files or refresh
89
- checksum-safe managed files. Customized files are reported but never
90
- overwritten. Sync also removes checksum-valid retired managed artifacts while
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
- `agency work` supplies the checkout directly; plain OpenCode launches resolve a
414
- materialized execution-unit checkout through `agency context`. A multi-phase
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.67.0",
3
+ "version": "2.68.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
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. The
93
- managed plugin provides whole-workbase access and writable-checkout skills, and
94
- the /agency-debug TUI diagnostic reports integration state without changing
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
+ }