@markjaquith/agency 3.3.0 → 3.4.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 +12 -6
- package/package.json +1 -1
- package/src/services/WorktreeService.ts +26 -6
- package/src/workbase/AGENTS.md +13 -10
- package/src/workbase/opencode-plugin-file.ts +123 -28
package/README.md
CHANGED
|
@@ -364,16 +364,22 @@ before acting; a matching worker performs the task directly and must not invoke
|
|
|
364
364
|
generated prompts when their document paths, current directory, and active valid
|
|
365
365
|
context all agree. External session state is not part of this identity contract.
|
|
366
366
|
The managed OpenCode plugin validates the marker against Agency context, binds it
|
|
367
|
-
to the receiving OpenCode session, injects an explicit active-worker system
|
|
368
|
-
instruction
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
to the receiving OpenCode session, and injects an explicit active-worker system
|
|
368
|
+
instruction. Its V1 integration also restores Agency identity for that session's
|
|
369
|
+
shell environment. OpenCode V2's shell hook is location-scoped and does not
|
|
370
|
+
identify the invoking session, so Agency deliberately avoids leaking one
|
|
371
|
+
session's worker identity into another; the validated marker and injected system
|
|
372
|
+
instruction remain the V2 fallback when the long-lived server did not inherit
|
|
373
|
+
the client's launch environment.
|
|
371
374
|
The `opencode2` and `opencode` agents remain rooted in their task or epic
|
|
372
375
|
working directory so the workbase `AGENTS.md` and managed OpenCode config are
|
|
373
376
|
discovered normally.
|
|
374
377
|
Agency's managed OpenCode plugin grants the active workbase external-directory
|
|
375
|
-
access and
|
|
376
|
-
`.opencode/{skill,skills}`
|
|
378
|
+
access and exposes existing checkout-local `.claude/skills`, `.agents/skills`,
|
|
379
|
+
and `.opencode/{skill,skills}` definitions. V1 adds those source directories to
|
|
380
|
+
`skills.paths`; V2 registers their discovered skill definitions through the
|
|
381
|
+
plugin API and injects the managed Agency instructions through a session context
|
|
382
|
+
hook. The global Pi
|
|
377
383
|
extension provides equivalent whole-workbase context and additionally discovers
|
|
378
384
|
checkout-local `.pi/skills` through Pi's `resources_discover` lifecycle.
|
|
379
385
|
`agency work` supplies the checkout directly; plain OpenCode and Pi launches
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type { RepositoryReference } from "../workbase/schemas"
|
|
17
17
|
import type { BaseCommandOptions } from "../utils/command"
|
|
18
18
|
import { createLoggers } from "../utils/effect"
|
|
19
|
+
import { createProgress } from "../utils/progress"
|
|
19
20
|
import { withWorktreeLocks } from "./WorktreeLock"
|
|
20
21
|
import { VersionControlService } from "./VersionControlService"
|
|
21
22
|
import type {
|
|
@@ -257,7 +258,12 @@ const runPostCheckoutHook = (options: {
|
|
|
257
258
|
const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
|
|
258
259
|
|
|
259
260
|
const originRef = (ref: string) =>
|
|
260
|
-
ref
|
|
261
|
+
ref
|
|
262
|
+
.replace(/^refs\/remotes\/origin\//, "")
|
|
263
|
+
.replace(/^origin\//, "")
|
|
264
|
+
.replace(/^refs\/heads\//, "")
|
|
265
|
+
|
|
266
|
+
const gitFetchTimeoutMs = 4 * 60 * 1000
|
|
261
267
|
|
|
262
268
|
interface MaterializeOptions extends BaseCommandOptions {
|
|
263
269
|
readonly force?: boolean
|
|
@@ -882,6 +888,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
882
888
|
const tasks = yield* TaskService
|
|
883
889
|
const phases = yield* PhaseService
|
|
884
890
|
const { verboseLog } = createLoggers(options)
|
|
891
|
+
const progress = createProgress({
|
|
892
|
+
silent: options.silent || options.json,
|
|
893
|
+
})
|
|
885
894
|
const forwardCommandOutput =
|
|
886
895
|
options.verbose === true && !options.silent && !options.json
|
|
887
896
|
const { root, config } = yield* workbase.loadConfig(startPath)
|
|
@@ -1046,7 +1055,10 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1046
1055
|
"origin",
|
|
1047
1056
|
originRef(executionBase),
|
|
1048
1057
|
],
|
|
1049
|
-
{
|
|
1058
|
+
{
|
|
1059
|
+
captureOutput: true,
|
|
1060
|
+
timeoutMs: gitFetchTimeoutMs,
|
|
1061
|
+
},
|
|
1050
1062
|
)
|
|
1051
1063
|
if (
|
|
1052
1064
|
remoteBase.exitCode !== 0 ||
|
|
@@ -1102,7 +1114,10 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1102
1114
|
"origin",
|
|
1103
1115
|
originRef(checkout.ref),
|
|
1104
1116
|
],
|
|
1105
|
-
{
|
|
1117
|
+
{
|
|
1118
|
+
captureOutput: true,
|
|
1119
|
+
timeoutMs: gitFetchTimeoutMs,
|
|
1120
|
+
},
|
|
1106
1121
|
)
|
|
1107
1122
|
if (remote.exitCode === 0 && remote.stdout.trim())
|
|
1108
1123
|
commit = remote.stdout.trim().split(/\s+/)[0]
|
|
@@ -1165,7 +1180,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1165
1180
|
})
|
|
1166
1181
|
}
|
|
1167
1182
|
|
|
1168
|
-
const fetchOrigin = (ref
|
|
1183
|
+
const fetchOrigin = (ref: string) =>
|
|
1169
1184
|
Effect.gen(function* () {
|
|
1170
1185
|
const remote = yield* fs.runCommand(
|
|
1171
1186
|
[
|
|
@@ -1185,7 +1200,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1185
1200
|
repositoryPath,
|
|
1186
1201
|
"fetch",
|
|
1187
1202
|
"origin",
|
|
1188
|
-
|
|
1203
|
+
ref,
|
|
1189
1204
|
]
|
|
1190
1205
|
if (options.dryRun) {
|
|
1191
1206
|
operations.push({
|
|
@@ -1197,14 +1212,19 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1197
1212
|
return false
|
|
1198
1213
|
}
|
|
1199
1214
|
|
|
1215
|
+
const status = `Fetching '${ref}' for '${alias}'`
|
|
1216
|
+
progress.start(`${status}...`)
|
|
1200
1217
|
const fetch = yield* fs.runCommand(command, {
|
|
1201
1218
|
captureOutput: true,
|
|
1219
|
+
timeoutMs: gitFetchTimeoutMs,
|
|
1202
1220
|
})
|
|
1203
1221
|
if (fetch.exitCode !== 0) {
|
|
1222
|
+
progress.fail(`${status} failed`)
|
|
1204
1223
|
return yield* new WorktreeError({
|
|
1205
1224
|
message: `Failed to fetch '${alias}': ${fetch.stderr}`,
|
|
1206
1225
|
})
|
|
1207
1226
|
}
|
|
1227
|
+
progress.succeed(`${status} complete`)
|
|
1208
1228
|
operations.push({
|
|
1209
1229
|
action: "fetch",
|
|
1210
1230
|
repo: alias,
|
|
@@ -1321,7 +1341,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1321
1341
|
message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
|
|
1322
1342
|
})
|
|
1323
1343
|
}
|
|
1324
|
-
yield* fetchOrigin()
|
|
1344
|
+
yield* fetchOrigin(originRef(executionBase))
|
|
1325
1345
|
|
|
1326
1346
|
let args: string[]
|
|
1327
1347
|
let env: Record<string, string> | undefined
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -207,10 +207,11 @@ recursively launch. External session state is never part of worker identity. If
|
|
|
207
207
|
the prompt and context disagree, stop and ask the user rather than launching.
|
|
208
208
|
|
|
209
209
|
For OpenCode, Agency's managed plugin validates the generated marker against
|
|
210
|
-
`agency context`, binds that identity to the OpenCode session, injects an
|
|
211
|
-
active-worker system instruction
|
|
212
|
-
shell environment.
|
|
213
|
-
|
|
210
|
+
`agency context`, binds that identity to the OpenCode session, and injects an
|
|
211
|
+
active-worker system instruction. The V1 integration also supplies Agency
|
|
212
|
+
identity to that session's shell environment. OpenCode V2's shell hook does not
|
|
213
|
+
identify the invoking session, so the plugin does not leak one session's identity
|
|
214
|
+
into another and instead retains the validated prompt fallback.
|
|
214
215
|
|
|
215
216
|
## Closeout
|
|
216
217
|
|
|
@@ -238,9 +239,11 @@ a refinement loop, or pausing or handing off completed implementation work):
|
|
|
238
239
|
|
|
239
240
|
`agency integration status` reports `managed`, `drifted`, `customized`, or
|
|
240
241
|
`missing` generated files. Agency keeps these instructions in
|
|
241
|
-
`.agency/AGENTS.md`, and its managed OpenCode
|
|
242
|
+
`.agency/AGENTS.md`, and its managed OpenCode integration loads them automatically.
|
|
242
243
|
It also installs a managed server plugin that exposes skills from the
|
|
243
|
-
authoritative writable checkout
|
|
244
|
+
authoritative writable checkout. In OpenCode V2 it injects these managed
|
|
245
|
+
instructions through a session context hook because configured instruction paths
|
|
246
|
+
are not currently loaded. The V1 integration also registers a TUI companion
|
|
244
247
|
providing `/agency-debug` without submitting an LLM prompt.
|
|
245
248
|
The workbase-root `AGENTS.md`, when present, belongs entirely to the workbase
|
|
246
249
|
owner and composes with these instructions through OpenCode's normal discovery.
|
|
@@ -253,7 +256,7 @@ OpenCode can access the complete workbase tree, but this filesystem permission
|
|
|
253
256
|
does not expand Agency write authority beyond the checkout reported by
|
|
254
257
|
`agency context`. OpenCode remains rooted in the task or epic directory so the
|
|
255
258
|
workbase instructions and config compose normally. The managed plugin resolves
|
|
256
|
-
the writable checkout from launch context or `agency context`, then
|
|
257
|
-
supported skill directories through
|
|
258
|
-
checkout-local OpenCode configuration authoritative. Agents
|
|
259
|
-
authority reported by `agency context`.
|
|
259
|
+
the writable checkout from launch context or `agency context`, then exposes its
|
|
260
|
+
supported skill directories through the applicable OpenCode plugin API; this
|
|
261
|
+
does not make other checkout-local OpenCode configuration authoritative. Agents
|
|
262
|
+
must follow the authority reported by `agency context`.
|
|
@@ -6,10 +6,21 @@ const managedHeaderPattern =
|
|
|
6
6
|
const checksum = (content: string) =>
|
|
7
7
|
createHash("sha256").update(content).digest("hex")
|
|
8
8
|
|
|
9
|
-
const body = `import { existsSync, readFileSync } from "node:fs"
|
|
10
|
-
import { dirname, join, resolve, sep } from "node:path"
|
|
9
|
+
const body = `import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
10
|
+
import { basename, dirname, extname, join, resolve, sep } from "node:path"
|
|
11
11
|
import { fileURLToPath } from "node:url"
|
|
12
|
-
|
|
12
|
+
type V1Plugin = (input: { directory: string }) => Promise<Record<string, any>>
|
|
13
|
+
type V2PluginContext = {
|
|
14
|
+
location: { directory: string }
|
|
15
|
+
reference: { transform(callback: (editor: any) => void): Promise<unknown> }
|
|
16
|
+
skill: { transform(callback: (editor: any) => void): Promise<unknown> }
|
|
17
|
+
permission: {
|
|
18
|
+
hook(name: "evaluate", callback: (event: any) => void): Promise<unknown>
|
|
19
|
+
}
|
|
20
|
+
session: {
|
|
21
|
+
hook(name: string, callback: (event: any) => void): Promise<unknown>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
const workerLaunchPattern = /^Agency worker launch target: ([^.\\s]+)\\./
|
|
15
26
|
|
|
@@ -21,20 +32,6 @@ type AgencyContext = {
|
|
|
21
32
|
phase?: string
|
|
22
33
|
}
|
|
23
34
|
|
|
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
|
-
|
|
38
35
|
const contextTarget = (result: Record<string, any>): string | undefined => {
|
|
39
36
|
const target = result.target
|
|
40
37
|
if (target?.kind === "epic") return \`epic:\${target.epicId}\`
|
|
@@ -93,6 +90,54 @@ const checkoutSkillPaths = (checkout: string | undefined) => checkout
|
|
|
93
90
|
].filter(existsSync)
|
|
94
91
|
: []
|
|
95
92
|
|
|
93
|
+
const skillFiles = (source: string) => {
|
|
94
|
+
const files: string[] = []
|
|
95
|
+
const visit = (directory: string, root = false) => {
|
|
96
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
97
|
+
const path = join(directory, entry.name)
|
|
98
|
+
if (entry.isDirectory()) visit(path)
|
|
99
|
+
else if (
|
|
100
|
+
entry.isFile() &&
|
|
101
|
+
(entry.name === "SKILL.md" || (root && extname(entry.name) === ".md"))
|
|
102
|
+
) files.push(path)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
visit(source, true)
|
|
106
|
+
return files
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const unquote = (value: string) => {
|
|
110
|
+
const trimmed = value.trim()
|
|
111
|
+
if (
|
|
112
|
+
trimmed.length >= 2 &&
|
|
113
|
+
((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
114
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")))
|
|
115
|
+
) return trimmed.slice(1, -1)
|
|
116
|
+
return trimmed
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const skillInfo = (location: string) => {
|
|
120
|
+
const raw = readFileSync(location, "utf8")
|
|
121
|
+
const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/)
|
|
122
|
+
const frontmatter = match?.[1] ?? ""
|
|
123
|
+
const scalar = (name: string) => {
|
|
124
|
+
const value = frontmatter.match(new RegExp(
|
|
125
|
+
"^" + name + "[ \\t]*:[ \\t]*(.+)$", "m",
|
|
126
|
+
))?.[1]
|
|
127
|
+
return value ? unquote(value) : undefined
|
|
128
|
+
}
|
|
129
|
+
const id = basename(location) === "SKILL.md"
|
|
130
|
+
? basename(dirname(location))
|
|
131
|
+
: basename(location, extname(location))
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
name: scalar("name") ?? id,
|
|
135
|
+
description: scalar("description"),
|
|
136
|
+
location,
|
|
137
|
+
content: match ? raw.slice(match[0].length) : raw,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
96
141
|
const agencyContext = async (
|
|
97
142
|
directory: string,
|
|
98
143
|
useEnvironmentTarget = true,
|
|
@@ -133,11 +178,11 @@ const agencyContext = async (
|
|
|
133
178
|
}
|
|
134
179
|
}
|
|
135
180
|
|
|
136
|
-
const plugin:
|
|
181
|
+
const plugin: V1Plugin = async ({ directory }) => {
|
|
137
182
|
const workerSessions = new Map<string, AgencyContext>()
|
|
138
183
|
|
|
139
184
|
return {
|
|
140
|
-
config: async (config) => {
|
|
185
|
+
config: async (config: any) => {
|
|
141
186
|
const context = await agencyContext(directory).catch(() => undefined)
|
|
142
187
|
const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(directory)
|
|
143
188
|
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(directory, root)
|
|
@@ -171,14 +216,17 @@ const plugin: Plugin = async ({ directory }) => {
|
|
|
171
216
|
config.skills ??= {}
|
|
172
217
|
config.skills.paths = [...new Set([...(config.skills.paths ?? []), ...paths])]
|
|
173
218
|
},
|
|
174
|
-
"chat.message": async ({ sessionID }, output) => {
|
|
219
|
+
"chat.message": async ({ sessionID }: { sessionID: string }, output: any) => {
|
|
175
220
|
const launchTarget = workerLaunchTarget(output.parts)
|
|
176
221
|
if (!launchTarget) return
|
|
177
222
|
const context = await agencyContext(directory, false).catch(() => undefined)
|
|
178
223
|
if (context?.target !== launchTarget) return
|
|
179
224
|
workerSessions.set(sessionID, context)
|
|
180
225
|
},
|
|
181
|
-
"experimental.chat.system.transform": async (
|
|
226
|
+
"experimental.chat.system.transform": async (
|
|
227
|
+
{ sessionID }: { sessionID?: string },
|
|
228
|
+
output: any,
|
|
229
|
+
) => {
|
|
182
230
|
if (!sessionID) return
|
|
183
231
|
const context = workerSessions.get(sessionID)
|
|
184
232
|
if (!context?.target) return
|
|
@@ -191,7 +239,7 @@ const plugin: Plugin = async ({ directory }) => {
|
|
|
191
239
|
].filter(Boolean).join(" "),
|
|
192
240
|
)
|
|
193
241
|
},
|
|
194
|
-
"shell.env": async ({ sessionID }, output) => {
|
|
242
|
+
"shell.env": async ({ sessionID }: { sessionID?: string }, output: any) => {
|
|
195
243
|
if (!sessionID) return
|
|
196
244
|
const context = workerSessions.get(sessionID)
|
|
197
245
|
if (!context?.target) return
|
|
@@ -209,9 +257,17 @@ const plugin: Plugin = async ({ directory }) => {
|
|
|
209
257
|
export const AgencyPlugin = plugin
|
|
210
258
|
|
|
211
259
|
const setup = async (context: V2PluginContext) => {
|
|
212
|
-
const
|
|
260
|
+
const directory = context.location.directory
|
|
261
|
+
const agency = await agencyContext(directory).catch(() => undefined)
|
|
262
|
+
const root = agency?.root ?? resolve(dirname(fileURLToPath(import.meta.url)), "../..")
|
|
263
|
+
const instructionsPath = join(root, ".agency", "AGENTS.md")
|
|
264
|
+
const instructions = existsSync(instructionsPath)
|
|
265
|
+
? readFileSync(instructionsPath, "utf8")
|
|
266
|
+
: undefined
|
|
213
267
|
await context.reference.transform((references) => {
|
|
214
|
-
if (
|
|
268
|
+
if (
|
|
269
|
+
references.list().some(([name]: [string, unknown]) => name === "workbase")
|
|
270
|
+
) return
|
|
215
271
|
references.add("workbase", {
|
|
216
272
|
type: "local",
|
|
217
273
|
path: root,
|
|
@@ -219,10 +275,49 @@ const setup = async (context: V2PluginContext) => {
|
|
|
219
275
|
})
|
|
220
276
|
})
|
|
221
277
|
|
|
222
|
-
const paths = checkoutSkillPaths(
|
|
223
|
-
|
|
278
|
+
const paths = checkoutSkillPaths(
|
|
279
|
+
process.env.AGENCY_WRITABLE_CHECKOUT ??
|
|
280
|
+
agency?.checkout ??
|
|
281
|
+
discoverCheckout(directory, root),
|
|
282
|
+
)
|
|
224
283
|
await context.skill.transform((skills) => {
|
|
225
|
-
for (const path of paths)
|
|
284
|
+
for (const path of paths) {
|
|
285
|
+
for (const location of skillFiles(path)) {
|
|
286
|
+
skills.add(skillInfo(location) as unknown as Parameters<typeof skills.add>[0])
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const workerSessions = new Map<string, AgencyContext>()
|
|
292
|
+
await context.permission.hook("evaluate", (event) => {
|
|
293
|
+
if (
|
|
294
|
+
event.action === "external_directory" &&
|
|
295
|
+
event.resources.every((resource: string) => {
|
|
296
|
+
const path = resolve(resource)
|
|
297
|
+
return path === root || path.startsWith(root + sep)
|
|
298
|
+
})
|
|
299
|
+
) event.effect = "allow"
|
|
300
|
+
})
|
|
301
|
+
await context.session.hook("prompt", async (event) => {
|
|
302
|
+
const launchTarget = event.prompt.text.match(workerLaunchPattern)?.[1]
|
|
303
|
+
if (!launchTarget) return
|
|
304
|
+
const current = await agencyContext(directory, false).catch(() => undefined)
|
|
305
|
+
if (!current || current.target !== launchTarget) return
|
|
306
|
+
workerSessions.set(event.sessionID, current)
|
|
307
|
+
})
|
|
308
|
+
await context.session.hook("context", (event) => {
|
|
309
|
+
if (instructions) event.system.push({ type: "text", text: instructions })
|
|
310
|
+
const current = workerSessions.get(event.sessionID)
|
|
311
|
+
if (!current?.target) return
|
|
312
|
+
event.system.push({
|
|
313
|
+
type: "text",
|
|
314
|
+
text: [
|
|
315
|
+
\`Agency verified this OpenCode session as the active worker for \${current.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`,
|
|
316
|
+
current.checkout
|
|
317
|
+
? \`OpenCode remains rooted in the task or phase directory for Agency instructions and context. Treat \${current.checkout} as the default implementation directory: use it for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Set each tool's working directory to that checkout when supported; otherwise use absolute paths. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.\`
|
|
318
|
+
: undefined,
|
|
319
|
+
].filter(Boolean).join(" "),
|
|
320
|
+
})
|
|
226
321
|
})
|
|
227
322
|
}
|
|
228
323
|
|
|
@@ -230,7 +325,7 @@ export default {
|
|
|
230
325
|
id: "agency",
|
|
231
326
|
setup,
|
|
232
327
|
server: plugin,
|
|
233
|
-
}
|
|
328
|
+
}
|
|
234
329
|
`
|
|
235
330
|
|
|
236
331
|
const renderManagedWorkbaseOpencodePlugin = (content: string) =>
|