@markjaquith/agency 2.54.3 → 2.54.4
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 +13 -0
- package/package.json +1 -1
- package/src/cli.test.ts +1 -1
- package/src/commands/work.test.ts +23 -9
- package/src/commands/work.ts +1 -0
- package/src/services/IntegrationService.test.ts +150 -0
- package/src/workbase/AGENTS.md +22 -0
- package/src/workbase/opencode-plugin-file.ts +130 -45
- package/src/workbase/runner-command.test.ts +3 -0
package/README.md
CHANGED
|
@@ -276,6 +276,19 @@ Execution-unit runners also receive `AGENCY_WRITABLE_CHECKOUT` with the
|
|
|
276
276
|
authoritative writable checkout path.
|
|
277
277
|
`AGENCY_CLAIM_REVISION` is empty for local `agency work` launches.
|
|
278
278
|
`AGENCY_PROMPT` is empty unless `--auto` is set.
|
|
279
|
+
Autonomous prompts begin `Agency worker launch target: <target>.`, carrying the
|
|
280
|
+
same canonical target as `AGENCY_TARGET`. This is the process-local fallback for
|
|
281
|
+
runner clients that attach to a long-lived process and lose launch environment
|
|
282
|
+
variables. A worker must verify either signal against `agency context . --json`
|
|
283
|
+
before acting; a matching worker performs the task directly and must not invoke
|
|
284
|
+
`agency work` for the same target. Managed guidance also fails safe for older
|
|
285
|
+
generated prompts when their document paths, current directory, and active valid
|
|
286
|
+
context all agree. Herdr is not part of this identity contract.
|
|
287
|
+
The managed OpenCode plugin validates the marker against Agency context, binds it
|
|
288
|
+
to the receiving OpenCode session, injects an explicit active-worker system
|
|
289
|
+
instruction, and restores Agency identity for that session's shell environment.
|
|
290
|
+
This session bridge is necessary because an OpenCode client can attach to a
|
|
291
|
+
long-lived server process that did not inherit the client's launch environment.
|
|
279
292
|
The `opencode` runner remains rooted in its task or epic working directory so
|
|
280
293
|
the workbase `AGENTS.md` and managed OpenCode config are discovered normally.
|
|
281
294
|
Agency's managed OpenCode plugin grants the active workbase external-directory
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -1177,7 +1177,7 @@ status: open
|
|
|
1177
1177
|
cwd: contract.cwd,
|
|
1178
1178
|
env: environment,
|
|
1179
1179
|
})
|
|
1180
|
-
expect(probe.exitCode).toBe(0)
|
|
1180
|
+
expect(probe.exitCode, probe.stderr.toString()).toBe(0)
|
|
1181
1181
|
const agent = JSON.parse(probe.stdout.toString())
|
|
1182
1182
|
expect(agent.permission).toEqual(
|
|
1183
1183
|
expect.arrayContaining([
|
|
@@ -98,6 +98,8 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
98
98
|
cwd: string
|
|
99
99
|
}> = []
|
|
100
100
|
const launchEnvironments: Array<Readonly<Record<string, string>>> = []
|
|
101
|
+
const launchProcessEnvironments: Array<Record<string, string | undefined>> =
|
|
102
|
+
[]
|
|
101
103
|
const materializeOptions: Array<
|
|
102
104
|
Parameters<WorktreeService["materialize"]>[3]
|
|
103
105
|
> = []
|
|
@@ -265,6 +267,11 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
265
267
|
events.push(`launch:${cli}`)
|
|
266
268
|
launches.push({ cli, args, cwd })
|
|
267
269
|
launchEnvironments.push(environment)
|
|
270
|
+
launchProcessEnvironments.push({
|
|
271
|
+
AGENCY_SESSION_ID: process.env.AGENCY_SESSION_ID,
|
|
272
|
+
AGENCY_TARGET: process.env.AGENCY_TARGET,
|
|
273
|
+
AGENCY_PROMPT: process.env.AGENCY_PROMPT,
|
|
274
|
+
})
|
|
268
275
|
if (options.launchError) throw options.launchError
|
|
269
276
|
}
|
|
270
277
|
const defaultPick: PickWorkTarget = () => Effect.succeed(null)
|
|
@@ -307,6 +314,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
307
314
|
probes,
|
|
308
315
|
launches,
|
|
309
316
|
launchEnvironments,
|
|
317
|
+
launchProcessEnvironments,
|
|
310
318
|
materializeOptions,
|
|
311
319
|
statusUpdates,
|
|
312
320
|
taskStatuses,
|
|
@@ -484,7 +492,7 @@ describe("work command", () => {
|
|
|
484
492
|
args: [
|
|
485
493
|
"opencode",
|
|
486
494
|
"--prompt",
|
|
487
|
-
"Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
|
|
495
|
+
"Agency worker launch target: epic:delivery. Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
|
|
488
496
|
],
|
|
489
497
|
cwd: "/workbase/epics/delivery",
|
|
490
498
|
})
|
|
@@ -538,7 +546,7 @@ describe("work command", () => {
|
|
|
538
546
|
args: [
|
|
539
547
|
"opencode",
|
|
540
548
|
"--prompt",
|
|
541
|
-
"Work on the task. Read /workbase/tasks/delivery/TASK.md.",
|
|
549
|
+
"Agency worker launch target: task:delivery. Work on the task. Read /workbase/tasks/delivery/TASK.md.",
|
|
542
550
|
],
|
|
543
551
|
cwd: "/workbase/tasks/delivery",
|
|
544
552
|
})
|
|
@@ -565,7 +573,7 @@ describe("work command", () => {
|
|
|
565
573
|
args: [
|
|
566
574
|
"opencode",
|
|
567
575
|
"--prompt",
|
|
568
|
-
"Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
576
|
+
"Agency worker launch target: execution-unit:phase/example/implementation. Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
569
577
|
],
|
|
570
578
|
cwd: phaseDirectory,
|
|
571
579
|
})
|
|
@@ -797,11 +805,17 @@ describe("work command", () => {
|
|
|
797
805
|
expect(harness.launches[0]?.args).toEqual([
|
|
798
806
|
"opencode",
|
|
799
807
|
"--prompt",
|
|
800
|
-
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
808
|
+
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
801
809
|
])
|
|
802
810
|
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe(
|
|
803
|
-
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
811
|
+
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
804
812
|
)
|
|
813
|
+
expect(harness.launchProcessEnvironments[0]).toEqual({
|
|
814
|
+
AGENCY_SESSION_ID: harness.launchEnvironments[0]?.AGENCY_SESSION_ID,
|
|
815
|
+
AGENCY_TARGET: "execution-unit:task/example",
|
|
816
|
+
AGENCY_PROMPT:
|
|
817
|
+
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
818
|
+
})
|
|
805
819
|
})
|
|
806
820
|
|
|
807
821
|
test("continues existing work with the resume command", async () => {
|
|
@@ -813,10 +827,10 @@ describe("work command", () => {
|
|
|
813
827
|
"opencode",
|
|
814
828
|
"--continue",
|
|
815
829
|
"--prompt",
|
|
816
|
-
"Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
830
|
+
"Agency worker launch target: execution-unit:task/example. Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
817
831
|
])
|
|
818
832
|
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe(
|
|
819
|
-
"Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
833
|
+
"Agency worker launch target: execution-unit:task/example. Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
820
834
|
)
|
|
821
835
|
})
|
|
822
836
|
|
|
@@ -840,7 +854,7 @@ describe("work command", () => {
|
|
|
840
854
|
"opencode",
|
|
841
855
|
"--continue",
|
|
842
856
|
"--prompt",
|
|
843
|
-
"Continue the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
857
|
+
"Agency worker launch target: execution-unit:phase/example/implementation. Continue the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
844
858
|
],
|
|
845
859
|
cwd: phaseDirectory,
|
|
846
860
|
})
|
|
@@ -870,7 +884,7 @@ describe("work command", () => {
|
|
|
870
884
|
"codex",
|
|
871
885
|
"--task",
|
|
872
886
|
"example",
|
|
873
|
-
"Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
887
|
+
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
874
888
|
],
|
|
875
889
|
cwd: taskDirectory,
|
|
876
890
|
})
|
package/src/commands/work.ts
CHANGED
|
@@ -264,6 +264,7 @@ export const work = (
|
|
|
264
264
|
launchPath = dirname(workspace.phasePath ?? workspace.taskPath)
|
|
265
265
|
writablePath = workspace.writablePath ?? undefined
|
|
266
266
|
}
|
|
267
|
+
prompt = `Agency worker launch target: ${targetNodeId(target)}. ${prompt}`
|
|
267
268
|
|
|
268
269
|
const explicitlyRequested = Boolean(
|
|
269
270
|
options.runner ||
|
|
@@ -129,6 +129,36 @@ describe("IntegrationService", () => {
|
|
|
129
129
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
130
130
|
"config.skills.paths = [...new Set",
|
|
131
131
|
)
|
|
132
|
+
expect(managedWorkbaseOpencodePlugin).toContain('"chat.message"')
|
|
133
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
134
|
+
"if (!Array.isArray(parts)) return",
|
|
135
|
+
)
|
|
136
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
137
|
+
'"experimental.chat.system.transform"',
|
|
138
|
+
)
|
|
139
|
+
expect(managedWorkbaseOpencodePlugin).toContain('"shell.env"')
|
|
140
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
141
|
+
"if (context?.target !== launchTarget) return",
|
|
142
|
+
)
|
|
143
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
144
|
+
"agencyContext(directory, false)",
|
|
145
|
+
)
|
|
146
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
147
|
+
"result.validation?.valid !== true",
|
|
148
|
+
)
|
|
149
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
150
|
+
"dirname(document) !== resolve(directory)",
|
|
151
|
+
)
|
|
152
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
153
|
+
"!result.authority?.writable?.checkoutPath",
|
|
154
|
+
)
|
|
155
|
+
expect(managedWorkbaseOpencodePlugin).toContain('status !== "working"')
|
|
156
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
157
|
+
"output.env.AGENCY_SESSION_ID = sessionID",
|
|
158
|
+
)
|
|
159
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
160
|
+
"Do not invoke agency work for this target",
|
|
161
|
+
)
|
|
132
162
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
133
163
|
".map((path) => `${path}${sep}.`)",
|
|
134
164
|
)
|
|
@@ -142,6 +172,121 @@ describe("IntegrationService", () => {
|
|
|
142
172
|
).toBe(false)
|
|
143
173
|
})
|
|
144
174
|
|
|
175
|
+
test("binds validated worker identity to an OpenCode session", async () => {
|
|
176
|
+
const path = join(root, "agency-repository-skills.ts")
|
|
177
|
+
const contextResponse = JSON.stringify({
|
|
178
|
+
version: 1,
|
|
179
|
+
ok: true,
|
|
180
|
+
result: {
|
|
181
|
+
workbase: { root },
|
|
182
|
+
target: {
|
|
183
|
+
kind: "task",
|
|
184
|
+
taskId: "example",
|
|
185
|
+
path: join(root, "TASK.md"),
|
|
186
|
+
},
|
|
187
|
+
authority: {
|
|
188
|
+
mode: "execution",
|
|
189
|
+
writable: { checkoutPath: join(root, "tasks/example/code/agency") },
|
|
190
|
+
},
|
|
191
|
+
documents: { task: { data: { status: "working" } } },
|
|
192
|
+
validation: { valid: true, warnings: [] },
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
await Bun.write(
|
|
196
|
+
path,
|
|
197
|
+
managedWorkbaseOpencodePlugin
|
|
198
|
+
.replace("const contextTarget =", "export const contextTarget =")
|
|
199
|
+
.replace("const agencyContext =", "export const agencyContext =")
|
|
200
|
+
.replace(
|
|
201
|
+
"const workerLaunchTarget =",
|
|
202
|
+
"export const workerLaunchTarget =",
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
const originalSpawn = Bun.spawn
|
|
206
|
+
Bun.spawn = (() =>
|
|
207
|
+
({
|
|
208
|
+
stdout: new Response(contextResponse).body!,
|
|
209
|
+
exited: Promise.resolve(0),
|
|
210
|
+
}) as ReturnType<typeof Bun.spawn>) as typeof Bun.spawn
|
|
211
|
+
try {
|
|
212
|
+
const generated = await import(
|
|
213
|
+
`${pathToFileURL(path).href}?worker-identity`
|
|
214
|
+
)
|
|
215
|
+
expect(
|
|
216
|
+
generated.workerLaunchTarget([
|
|
217
|
+
{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: "Agency worker launch target: execution-unit:task/example. Start the task.",
|
|
220
|
+
},
|
|
221
|
+
]),
|
|
222
|
+
).toBe("execution-unit:task/example")
|
|
223
|
+
expect(generated.workerLaunchTarget(undefined)).toBeUndefined()
|
|
224
|
+
expect(
|
|
225
|
+
generated.contextTarget({
|
|
226
|
+
target: { kind: "task", taskId: "example" },
|
|
227
|
+
authority: { mode: "execution" },
|
|
228
|
+
}),
|
|
229
|
+
).toBe("execution-unit:task/example")
|
|
230
|
+
expect(await generated.agencyContext(root)).toMatchObject({
|
|
231
|
+
root,
|
|
232
|
+
target: "execution-unit:task/example",
|
|
233
|
+
task: "example",
|
|
234
|
+
})
|
|
235
|
+
const hooks = await generated.default({ directory: root } as never)
|
|
236
|
+
await hooks["chat.message"]!(
|
|
237
|
+
{ sessionID: "worker-session" } as never,
|
|
238
|
+
{
|
|
239
|
+
parts: [
|
|
240
|
+
{
|
|
241
|
+
type: "text",
|
|
242
|
+
text: "Agency worker launch target: execution-unit:task/example. Start the task.",
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
} as never,
|
|
246
|
+
)
|
|
247
|
+
await hooks["chat.message"]!(
|
|
248
|
+
{ sessionID: "mismatched-session" } as never,
|
|
249
|
+
{
|
|
250
|
+
parts: [
|
|
251
|
+
{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: "Agency worker launch target: execution-unit:task/other. Start the task.",
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
} as never,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
const system = { system: [] as string[] }
|
|
260
|
+
await hooks["experimental.chat.system.transform"]!(
|
|
261
|
+
{ sessionID: "worker-session" } as never,
|
|
262
|
+
system,
|
|
263
|
+
)
|
|
264
|
+
expect(system.system).toEqual([
|
|
265
|
+
expect.stringContaining(
|
|
266
|
+
"active worker for execution-unit:task/example",
|
|
267
|
+
),
|
|
268
|
+
])
|
|
269
|
+
const mismatchedSystem = { system: [] as string[] }
|
|
270
|
+
await hooks["experimental.chat.system.transform"]!(
|
|
271
|
+
{ sessionID: "mismatched-session" } as never,
|
|
272
|
+
mismatchedSystem,
|
|
273
|
+
)
|
|
274
|
+
expect(mismatchedSystem.system).toEqual([])
|
|
275
|
+
|
|
276
|
+
const shell = { env: {} as Record<string, string> }
|
|
277
|
+
await hooks["shell.env"]!({ sessionID: "worker-session" } as never, shell)
|
|
278
|
+
expect(shell.env).toMatchObject({
|
|
279
|
+
AGENCY_SESSION_ID: "worker-session",
|
|
280
|
+
AGENCY_TARGET: "execution-unit:task/example",
|
|
281
|
+
AGENCY_WORKBASE: root,
|
|
282
|
+
AGENCY_TASK_ID: "example",
|
|
283
|
+
AGENCY_WRITABLE_CHECKOUT: join(root, "tasks/example/code/agency"),
|
|
284
|
+
})
|
|
285
|
+
} finally {
|
|
286
|
+
Bun.spawn = originalSpawn
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
145
290
|
test("registers a TUI-only /agency-debug diagnostic", async () => {
|
|
146
291
|
const config = JSON.parse(managedBody(managedWorkbaseOpencodeTui))
|
|
147
292
|
expect(config).toEqual({
|
|
@@ -268,6 +413,11 @@ describe("IntegrationService", () => {
|
|
|
268
413
|
expect(body).toContain("Preserve parent backlinks")
|
|
269
414
|
expect(body).toContain("dirty-worktree, active-claim, revision")
|
|
270
415
|
expect(body).toContain("`agency work` is the human launch flow")
|
|
416
|
+
expect(body).toContain("Agency worker launch target: <target>.")
|
|
417
|
+
expect(body).toContain("environment variables and a generated")
|
|
418
|
+
expect(body).toContain("the initial instruction is a generated")
|
|
419
|
+
expect(body).toContain("Herdr state is never part of worker identity")
|
|
420
|
+
expect(body).toMatch(/If the prompt\s+and context disagree/)
|
|
271
421
|
expect(body).toContain("marks execution work")
|
|
272
422
|
expect(body).toContain("without creating a claim")
|
|
273
423
|
expect(body).toContain("formatting, type checks, build, dead-code checks")
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -76,6 +76,28 @@ task launches remain orchestration-only. External orchestrators instead claim
|
|
|
76
76
|
an execution unit, launch and monitor their runner separately, and finish or
|
|
77
77
|
release the claim with the current document revision.
|
|
78
78
|
|
|
79
|
+
An Agency-launched runner receives process-local worker identity through both
|
|
80
|
+
the `AGENCY_SESSION_ID` and `AGENCY_TARGET` environment variables and a generated
|
|
81
|
+
prompt beginning `Agency worker launch target: <target>.` Treat either form as
|
|
82
|
+
launch evidence only after `agency context . --json` confirms the same target,
|
|
83
|
+
document paths, valid context, and expected write authority. Once confirmed,
|
|
84
|
+
perform the assigned work directly and never invoke `agency work` to start the
|
|
85
|
+
same target again.
|
|
86
|
+
|
|
87
|
+
Some runner clients attach to a long-lived process and may not preserve launch
|
|
88
|
+
environment variables. If the variables and prompt marker are absent, fail safe
|
|
89
|
+
when the initial instruction is a generated `Start`, `Continue`, or `Work on`
|
|
90
|
+
prompt whose absolute document paths match the current directory and the active,
|
|
91
|
+
valid `agency context`: treat the process as the current worker and do not
|
|
92
|
+
recursively launch. Herdr state is never part of worker identity. If the prompt
|
|
93
|
+
and context disagree, stop and ask the user rather than launching.
|
|
94
|
+
|
|
95
|
+
For OpenCode, Agency's managed plugin validates the generated marker against
|
|
96
|
+
`agency context`, binds that identity to the OpenCode session, injects an
|
|
97
|
+
active-worker system instruction, and supplies Agency identity to that session's
|
|
98
|
+
shell environment. This avoids relying on the environment of OpenCode's
|
|
99
|
+
long-lived server process.
|
|
100
|
+
|
|
79
101
|
## Closeout
|
|
80
102
|
|
|
81
103
|
An execution unit remains `working` after implementation is committed and while
|
|
@@ -7,9 +7,41 @@ const checksum = (content: string) =>
|
|
|
7
7
|
createHash("sha256").update(content).digest("hex")
|
|
8
8
|
|
|
9
9
|
const body = `import { existsSync, readFileSync } from "node:fs"
|
|
10
|
-
import { dirname, join, sep } from "node:path"
|
|
10
|
+
import { dirname, join, resolve, sep } from "node:path"
|
|
11
11
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
12
12
|
|
|
13
|
+
const workerLaunchPattern = /^Agency worker launch target: ([^.\\s]+)\\./
|
|
14
|
+
|
|
15
|
+
type AgencyContext = {
|
|
16
|
+
root?: string
|
|
17
|
+
checkout?: string
|
|
18
|
+
target?: string
|
|
19
|
+
task?: string
|
|
20
|
+
phase?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const contextTarget = (result: Record<string, any>): string | undefined => {
|
|
24
|
+
const target = result.target
|
|
25
|
+
if (target?.kind === "epic") return \`epic:\${target.epicId}\`
|
|
26
|
+
if (target?.kind === "phase") {
|
|
27
|
+
return \`execution-unit:phase/\${target.taskId}/\${target.phaseId}\`
|
|
28
|
+
}
|
|
29
|
+
if (target?.kind === "task") {
|
|
30
|
+
return result.authority?.mode === "execution"
|
|
31
|
+
? \`execution-unit:task/\${target.taskId}\`
|
|
32
|
+
: \`task:\${target.taskId}\`
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const workerLaunchTarget = (parts: unknown) => {
|
|
37
|
+
if (!Array.isArray(parts)) return
|
|
38
|
+
const text = parts
|
|
39
|
+
.filter((part) => part.type === "text")
|
|
40
|
+
.map((part) => part.text ?? "")
|
|
41
|
+
.join("\\n")
|
|
42
|
+
return text.match(workerLaunchPattern)?.[1]
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
const discoverWorkbase = (directory: string) => {
|
|
14
46
|
let current = directory
|
|
15
47
|
while (true) {
|
|
@@ -37,64 +69,117 @@ const discoverCheckout = (directory: string, root: string | undefined) => {
|
|
|
37
69
|
}
|
|
38
70
|
}
|
|
39
71
|
|
|
40
|
-
const agencyContext = async (
|
|
41
|
-
|
|
42
|
-
|
|
72
|
+
const agencyContext = async (
|
|
73
|
+
directory: string,
|
|
74
|
+
useEnvironmentTarget = true,
|
|
75
|
+
): Promise<AgencyContext | undefined> => {
|
|
76
|
+
const task = useEnvironmentTarget ? process.env.AGENCY_TASK_ID : undefined
|
|
77
|
+
const phase = useEnvironmentTarget ? process.env.AGENCY_PHASE_ID : undefined
|
|
43
78
|
const args = task
|
|
44
79
|
? ["agency", "context", "--task", task, ...(phase ? ["--phase", phase] : []), "--compact", "--json"]
|
|
45
80
|
: ["agency", "context", ".", "--compact", "--json"]
|
|
46
|
-
const child = Bun.spawn(args, {
|
|
81
|
+
const child = Bun.spawn(args, {
|
|
82
|
+
cwd: directory,
|
|
83
|
+
env: process.env,
|
|
84
|
+
stdout: "pipe",
|
|
85
|
+
stderr: "ignore",
|
|
86
|
+
})
|
|
47
87
|
const output = await new Response(child.stdout).text()
|
|
48
88
|
if ((await child.exited) !== 0) return
|
|
49
89
|
const envelope = JSON.parse(output)
|
|
50
90
|
if (envelope.ok !== true) return
|
|
91
|
+
const result = envelope.result ?? {}
|
|
92
|
+
const target = contextTarget(result)
|
|
93
|
+
const document = result.target?.path
|
|
94
|
+
const status = result.documents?.phase?.data?.status ?? result.documents?.task?.data?.status
|
|
95
|
+
if (
|
|
96
|
+
result.validation?.valid !== true ||
|
|
97
|
+
!target ||
|
|
98
|
+
!document ||
|
|
99
|
+
dirname(document) !== resolve(directory) ||
|
|
100
|
+
(target.startsWith("execution-unit:") &&
|
|
101
|
+
(!result.authority?.writable?.checkoutPath || status !== "working"))
|
|
102
|
+
) return
|
|
51
103
|
return {
|
|
52
|
-
root:
|
|
53
|
-
checkout:
|
|
104
|
+
root: result.workbase?.root,
|
|
105
|
+
checkout: result.authority?.writable?.checkoutPath,
|
|
106
|
+
target,
|
|
107
|
+
task: result.target?.taskId,
|
|
108
|
+
phase: result.target?.phaseId,
|
|
54
109
|
}
|
|
55
110
|
}
|
|
56
111
|
|
|
57
|
-
const plugin: Plugin = async ({ directory }) =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
config.permission
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
[join(root, "*")]: "allow"
|
|
79
|
-
|
|
112
|
+
const plugin: Plugin = async ({ directory }) => {
|
|
113
|
+
const workerSessions = new Map<string, AgencyContext>()
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
config: async (config) => {
|
|
117
|
+
const context = await agencyContext(directory).catch(() => undefined)
|
|
118
|
+
const root = process.env.AGENCY_WORKBASE ?? context?.root ?? discoverWorkbase(directory)
|
|
119
|
+
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout ?? discoverCheckout(directory, root)
|
|
120
|
+
|
|
121
|
+
const reference = config.references?.workbase
|
|
122
|
+
if (
|
|
123
|
+
root &&
|
|
124
|
+
typeof reference === "object" &&
|
|
125
|
+
reference.path === ".." &&
|
|
126
|
+
reference.description ===
|
|
127
|
+
"Complete Agency workbase context; write authority still comes only from agency context" &&
|
|
128
|
+
typeof config.permission !== "string"
|
|
129
|
+
) {
|
|
130
|
+
config.permission ??= {}
|
|
131
|
+
const external = config.permission.external_directory
|
|
132
|
+
if (external === undefined) {
|
|
133
|
+
config.permission.external_directory = { [join(root, "*")]: "allow" }
|
|
134
|
+
} else if (typeof external === "object") {
|
|
135
|
+
config.permission.external_directory = {
|
|
136
|
+
[join(root, "*")]: "allow",
|
|
137
|
+
...external,
|
|
138
|
+
}
|
|
80
139
|
}
|
|
81
140
|
}
|
|
82
|
-
}
|
|
83
141
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
})
|
|
142
|
+
if (!checkout) return
|
|
143
|
+
|
|
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}.\`)
|
|
150
|
+
if (paths.length === 0) return
|
|
151
|
+
|
|
152
|
+
config.skills ??= {}
|
|
153
|
+
config.skills.paths = [...new Set([...(config.skills.paths ?? []), ...paths])]
|
|
154
|
+
},
|
|
155
|
+
"chat.message": async ({ sessionID }, output) => {
|
|
156
|
+
const launchTarget = workerLaunchTarget(output.parts)
|
|
157
|
+
if (!launchTarget) return
|
|
158
|
+
const context = await agencyContext(directory, false).catch(() => undefined)
|
|
159
|
+
if (context?.target !== launchTarget) return
|
|
160
|
+
workerSessions.set(sessionID, context)
|
|
161
|
+
},
|
|
162
|
+
"experimental.chat.system.transform": async ({ sessionID }, output) => {
|
|
163
|
+
if (!sessionID) return
|
|
164
|
+
const context = workerSessions.get(sessionID)
|
|
165
|
+
if (!context?.target) return
|
|
166
|
+
output.system.push(
|
|
167
|
+
\`Agency verified this OpenCode session as the active worker for \${context.target}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.\`,
|
|
168
|
+
)
|
|
169
|
+
},
|
|
170
|
+
"shell.env": async ({ sessionID }, output) => {
|
|
171
|
+
if (!sessionID) return
|
|
172
|
+
const context = workerSessions.get(sessionID)
|
|
173
|
+
if (!context?.target) return
|
|
174
|
+
output.env.AGENCY_SESSION_ID = sessionID
|
|
175
|
+
output.env.AGENCY_TARGET = context.target
|
|
176
|
+
if (context.root) output.env.AGENCY_WORKBASE = context.root
|
|
177
|
+
if (context.checkout) output.env.AGENCY_WRITABLE_CHECKOUT = context.checkout
|
|
178
|
+
if (context.task) output.env.AGENCY_TASK_ID = context.task
|
|
179
|
+
if (context.phase) output.env.AGENCY_PHASE_ID = context.phase
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
98
183
|
|
|
99
184
|
export default plugin
|
|
100
185
|
`
|
|
@@ -95,9 +95,12 @@ describe("runner commands", () => {
|
|
|
95
95
|
expect(environment).toMatchObject({
|
|
96
96
|
AGENCY_RUNNER: "custom",
|
|
97
97
|
AGENCY_CLAIMANT: "orchestrator",
|
|
98
|
+
AGENCY_SESSION_ID: "session-1",
|
|
99
|
+
AGENCY_WORKBASE: "/workbase",
|
|
98
100
|
AGENCY_TARGET: "execution-unit:phase/task/build",
|
|
99
101
|
AGENCY_TASK_ID: "task",
|
|
100
102
|
AGENCY_PHASE_ID: "build",
|
|
103
|
+
AGENCY_PROMPT: "Read the task.",
|
|
101
104
|
})
|
|
102
105
|
expect(printableEnvironment(environment).VISIBLE).toBe("yes")
|
|
103
106
|
expect(printableEnvironment(environment).ACCESS_TOKEN).toBeUndefined()
|