@markjaquith/agency 2.47.0 → 2.47.1
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 +8 -6
- package/package.json +1 -1
- package/skills/agency/references/recipes.md +2 -3
- package/src/cli.test.ts +35 -11
- package/src/commands/integration.test.ts +5 -5
- package/src/commands/integration.ts +5 -4
- package/src/commands/work.test.ts +6 -21
- package/src/commands/work.ts +4 -14
- package/src/services/IntegrationService.test.ts +4 -1
- package/src/services/IntegrationService.ts +3 -3
- package/src/workbase/opencode-plugin-file.ts +30 -5
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ workbase/
|
|
|
54
54
|
opencode.jsonc # managed @agency subagent, instructions, and reference
|
|
55
55
|
tui.jsonc # managed TUI plugin registration
|
|
56
56
|
command/agency.md # managed /agency workflow command
|
|
57
|
-
plugin/agency-repository-skills.ts # managed checkout
|
|
57
|
+
plugin/agency-repository-skills.ts # managed workbase access and checkout skills
|
|
58
58
|
tui/agency-debug.ts # managed /agency-debug TUI diagnostic
|
|
59
59
|
agency.json # tracked config and portable repository declarations
|
|
60
60
|
repos/ # ignored local materializations
|
|
@@ -111,9 +111,10 @@ whether the server plugin registered writable-checkout skills. It uses a native
|
|
|
111
111
|
toast and does not submit a prompt to an LLM. When no writable checkout skill
|
|
112
112
|
directory is available, server initialization is reported as indeterminate
|
|
113
113
|
rather than inferred from plugin discovery.
|
|
114
|
-
OpenCode discovers the config from task and epic launch directories.
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
OpenCode discovers the config and plugin from task and epic launch directories.
|
|
115
|
+
The plugin grants whole-workbase access dynamically, while the portable
|
|
116
|
+
reference advertises that context to agents. Bash and Agency operations must
|
|
117
|
+
still follow the write authority reported by `agency context`.
|
|
117
118
|
|
|
118
119
|
OpenCode also discovers a managed `/agency` command. Use `/agency status` for a
|
|
119
120
|
read-only current-work summary, `/agency start [target]` to begin or resume work
|
|
@@ -252,8 +253,9 @@ authoritative writable checkout path.
|
|
|
252
253
|
`AGENCY_PROMPT` is empty unless `--auto` is set.
|
|
253
254
|
The `opencode` runner remains rooted in its task or epic working directory so
|
|
254
255
|
the workbase `AGENTS.md` and managed OpenCode config are discovered normally.
|
|
255
|
-
Agency's managed OpenCode plugin
|
|
256
|
-
`.
|
|
256
|
+
Agency's managed OpenCode plugin grants the active workbase external-directory
|
|
257
|
+
access and adds existing checkout-local `.claude/skills`, `.agents/skills`, and
|
|
258
|
+
`.opencode/{skill,skills}` directories to `skills.paths`.
|
|
257
259
|
`agency work` supplies the checkout directly; plain OpenCode launches resolve a
|
|
258
260
|
materialized execution-unit checkout through `agency context`. A multi-phase
|
|
259
261
|
task root has no single checkout, so launch from its phase directory when using
|
package/package.json
CHANGED
|
@@ -92,9 +92,8 @@ checks readiness, materializes worktrees, marks this execution unit working
|
|
|
92
92
|
without a claim, and launches the runner. Run it again to relaunch unclaimed
|
|
93
93
|
working work. The runner opens without a prompt by default; add `--auto` only
|
|
94
94
|
when the generated context prompt should start autonomous execution.
|
|
95
|
-
OpenCode receives whole-workbase
|
|
96
|
-
|
|
97
|
-
`agency context`.
|
|
95
|
+
OpenCode receives whole-workbase visibility through the managed project plugin;
|
|
96
|
+
this does not expand the writable checkout reported by `agency context`.
|
|
98
97
|
|
|
99
98
|
## Active Agent: Execute Assigned Work
|
|
100
99
|
|
package/src/cli.test.ts
CHANGED
|
@@ -1059,13 +1059,7 @@ status: open
|
|
|
1059
1059
|
expect(contract.environment.AGENCY_WRITABLE_CHECKOUT).toBe(
|
|
1060
1060
|
launch.checkoutPath,
|
|
1061
1061
|
)
|
|
1062
|
-
expect(
|
|
1063
|
-
JSON.parse(contract.environment.OPENCODE_CONFIG_CONTENT),
|
|
1064
|
-
).toEqual({
|
|
1065
|
-
permission: {
|
|
1066
|
-
external_directory: { [join(workbaseRoot, "*")]: "allow" },
|
|
1067
|
-
},
|
|
1068
|
-
})
|
|
1062
|
+
expect(contract.environment.OPENCODE_CONFIG_CONTENT).toBeUndefined()
|
|
1069
1063
|
const environment = {
|
|
1070
1064
|
...process.env,
|
|
1071
1065
|
...contract.environment,
|
|
@@ -1093,6 +1087,9 @@ status: open
|
|
|
1093
1087
|
})
|
|
1094
1088
|
expect(configProbe.exitCode).toBe(0)
|
|
1095
1089
|
const effectiveConfig = JSON.parse(configProbe.stdout.toString())
|
|
1090
|
+
expect(effectiveConfig.permission.external_directory).toMatchObject({
|
|
1091
|
+
[join(workbaseRoot, "*")]: "allow",
|
|
1092
|
+
})
|
|
1096
1093
|
expect(effectiveConfig.plugin_origins).toEqual(
|
|
1097
1094
|
expect.arrayContaining([
|
|
1098
1095
|
expect.objectContaining({
|
|
@@ -1218,9 +1215,11 @@ status: open
|
|
|
1218
1215
|
env: directEnvironment,
|
|
1219
1216
|
})
|
|
1220
1217
|
expect(configProbe.exitCode).toBe(0)
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1218
|
+
const effectiveConfig = JSON.parse(configProbe.stdout.toString())
|
|
1219
|
+
expect(effectiveConfig.permission.external_directory).toMatchObject({
|
|
1220
|
+
[join(workbaseRoot, "*")]: "allow",
|
|
1221
|
+
})
|
|
1222
|
+
expect(effectiveConfig.plugin_origins).toEqual(
|
|
1224
1223
|
expect.arrayContaining([
|
|
1225
1224
|
expect.objectContaining({
|
|
1226
1225
|
spec: expect.stringContaining("agency-repository-skills.ts"),
|
|
@@ -1247,6 +1246,31 @@ status: open
|
|
|
1247
1246
|
)
|
|
1248
1247
|
}
|
|
1249
1248
|
}
|
|
1249
|
+
const denied = Bun.spawnSync(["opencode", "debug", "agent", "build"], {
|
|
1250
|
+
cwd: directDirectories[0],
|
|
1251
|
+
env: {
|
|
1252
|
+
...directEnvironment,
|
|
1253
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
1254
|
+
permission: { external_directory: { "*": "deny" } },
|
|
1255
|
+
}),
|
|
1256
|
+
},
|
|
1257
|
+
})
|
|
1258
|
+
expect(denied.exitCode).toBe(0)
|
|
1259
|
+
const deniedRules = JSON.parse(denied.stdout.toString()).permission
|
|
1260
|
+
const managedRule = deniedRules.findIndex(
|
|
1261
|
+
(rule: any) =>
|
|
1262
|
+
rule.permission === "external_directory" &&
|
|
1263
|
+
rule.pattern === join(workbaseRoot, "*") &&
|
|
1264
|
+
rule.action === "allow",
|
|
1265
|
+
)
|
|
1266
|
+
const userRule = deniedRules.findLastIndex(
|
|
1267
|
+
(rule: any) =>
|
|
1268
|
+
rule.permission === "external_directory" &&
|
|
1269
|
+
rule.pattern === "*" &&
|
|
1270
|
+
rule.action === "deny",
|
|
1271
|
+
)
|
|
1272
|
+
expect(managedRule).toBeGreaterThanOrEqual(0)
|
|
1273
|
+
expect(userRule).toBeGreaterThan(managedRule)
|
|
1250
1274
|
for (const cwd of [
|
|
1251
1275
|
join(workbaseRoot, "tasks/example"),
|
|
1252
1276
|
join(workbaseRoot, "tasks/pipeline/phases/build"),
|
|
@@ -1325,7 +1349,7 @@ status: open
|
|
|
1325
1349
|
graph.nodes.find((node: any) => node.id === "task:pipeline").status,
|
|
1326
1350
|
).toBe("working")
|
|
1327
1351
|
},
|
|
1328
|
-
|
|
1352
|
+
180_000,
|
|
1329
1353
|
)
|
|
1330
1354
|
|
|
1331
1355
|
test("envelopes help and version output in machine mode", async () => {
|
|
@@ -96,10 +96,10 @@ OpenCode /agency command: missing
|
|
|
96
96
|
The managed OpenCode /agency command needs synchronization.
|
|
97
97
|
Action: Run 'agency integration sync' to install the managed /agency command.
|
|
98
98
|
|
|
99
|
-
OpenCode
|
|
99
|
+
OpenCode workbase plugin: missing
|
|
100
100
|
Path: .opencode/plugin/agency-repository-skills.ts
|
|
101
|
-
The managed OpenCode
|
|
102
|
-
Action: Run 'agency integration sync' to expose writable-checkout skills in OpenCode.
|
|
101
|
+
The managed OpenCode workbase plugin needs synchronization.
|
|
102
|
+
Action: Run 'agency integration sync' to provide workbase access and expose writable-checkout skills in OpenCode.
|
|
103
103
|
|
|
104
104
|
OpenCode TUI config: missing
|
|
105
105
|
Path: .opencode/tui.jsonc
|
|
@@ -165,9 +165,9 @@ OpenCode /agency command: synced
|
|
|
165
165
|
Path: .opencode/command/agency.md
|
|
166
166
|
Agency's managed OpenCode /agency command is current.
|
|
167
167
|
|
|
168
|
-
OpenCode
|
|
168
|
+
OpenCode workbase plugin: synced
|
|
169
169
|
Path: .opencode/plugin/agency-repository-skills.ts
|
|
170
|
-
Agency's managed OpenCode plugin exposes writable-checkout skills
|
|
170
|
+
Agency's managed OpenCode plugin provides whole-workbase access and exposes writable-checkout skills.
|
|
171
171
|
|
|
172
172
|
OpenCode TUI config: synced
|
|
173
173
|
Path: .opencode/tui.jsonc
|
|
@@ -31,7 +31,7 @@ const integrationNames = {
|
|
|
31
31
|
agents: "Agent instructions",
|
|
32
32
|
opencode: "OpenCode config",
|
|
33
33
|
"opencode-command": "OpenCode /agency command",
|
|
34
|
-
"opencode-plugin": "OpenCode
|
|
34
|
+
"opencode-plugin": "OpenCode workbase plugin",
|
|
35
35
|
"opencode-tui": "OpenCode TUI config",
|
|
36
36
|
"opencode-tui-plugin": "OpenCode /agency-debug",
|
|
37
37
|
} as const
|
|
@@ -91,9 +91,10 @@ export const help = `
|
|
|
91
91
|
Usage: agency integration <subcommand>
|
|
92
92
|
|
|
93
93
|
Inspect or explicitly synchronize managed agent integration files. OpenCode
|
|
94
|
-
launches load the managed instructions and project config at runtime
|
|
95
|
-
|
|
96
|
-
diagnostic
|
|
94
|
+
launches load the managed instructions and project config at runtime. The
|
|
95
|
+
managed plugin provides whole-workbase access and writable-checkout skills, and
|
|
96
|
+
the /agency-debug TUI diagnostic reports integration state without changing
|
|
97
|
+
Agency write authority.
|
|
97
98
|
|
|
98
99
|
Subcommands:
|
|
99
100
|
status Report file state, access diagnostics, and safe remediation
|
|
@@ -45,10 +45,6 @@ const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const taskDirectory = "/workbase/tasks/example"
|
|
48
|
-
const workbasePermission = JSON.stringify({
|
|
49
|
-
permission: { external_directory: { "/workbase/*": "allow" } },
|
|
50
|
-
})
|
|
51
|
-
|
|
52
48
|
interface HarnessOptions {
|
|
53
49
|
readonly workspace?: ExecutionWorkspace
|
|
54
50
|
readonly materializeError?: Error
|
|
@@ -74,7 +70,6 @@ interface HarnessOptions {
|
|
|
74
70
|
readonly guardError?: Error
|
|
75
71
|
readonly launchError?: Error
|
|
76
72
|
readonly workTargetIds?: readonly string[]
|
|
77
|
-
readonly opencodeIntegrationState?: "managed" | "customized"
|
|
78
73
|
readonly taskStatus?: "open" | "working" | "delegated" | "done" | "dropped"
|
|
79
74
|
readonly phaseStatus?: "open" | "working" | "delegated" | "done" | "dropped"
|
|
80
75
|
readonly taskStatuses?: Readonly<
|
|
@@ -237,7 +232,7 @@ const createHarness = (options: HarnessOptions = {}) => {
|
|
|
237
232
|
files: [
|
|
238
233
|
{
|
|
239
234
|
name: "opencode",
|
|
240
|
-
state:
|
|
235
|
+
state: "managed",
|
|
241
236
|
},
|
|
242
237
|
],
|
|
243
238
|
})
|
|
@@ -490,9 +485,9 @@ describe("work command", () => {
|
|
|
490
485
|
cwd: "/workbase/epics/delivery",
|
|
491
486
|
})
|
|
492
487
|
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
493
|
-
expect(
|
|
494
|
-
|
|
495
|
-
)
|
|
488
|
+
expect(
|
|
489
|
+
harness.launchEnvironments[0]?.OPENCODE_CONFIG_CONTENT,
|
|
490
|
+
).toBeUndefined()
|
|
496
491
|
})
|
|
497
492
|
|
|
498
493
|
test("resolves an existing positional path before treating it as a task ID", async () => {
|
|
@@ -918,7 +913,7 @@ describe("work command", () => {
|
|
|
918
913
|
expect(harness.taskStatuses.example).toBe("done")
|
|
919
914
|
})
|
|
920
915
|
|
|
921
|
-
test("
|
|
916
|
+
test("leaves managed OpenCode access to the project plugin", async () => {
|
|
922
917
|
const harness = createHarness()
|
|
923
918
|
const output = await captureLogs(() =>
|
|
924
919
|
harness.run({ taskId: "example", opencode: true, printCommand: true }),
|
|
@@ -926,7 +921,7 @@ describe("work command", () => {
|
|
|
926
921
|
const printed = JSON.parse(output.join("\n"))
|
|
927
922
|
|
|
928
923
|
expect(printed.environment.OPENCODE_CONFIG).toBeUndefined()
|
|
929
|
-
expect(printed.environment.OPENCODE_CONFIG_CONTENT).
|
|
924
|
+
expect(printed.environment.OPENCODE_CONFIG_CONTENT).toBeUndefined()
|
|
930
925
|
})
|
|
931
926
|
|
|
932
927
|
test("provides the writable checkout to plugins without changing the project", async () => {
|
|
@@ -941,16 +936,6 @@ describe("work command", () => {
|
|
|
941
936
|
expect(harness.launchEnvironments[0]?.AGENCY_WRITABLE_CHECKOUT).toBe(
|
|
942
937
|
"/workbase/tasks/example/code/agency",
|
|
943
938
|
)
|
|
944
|
-
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG_CONTENT).toBe(
|
|
945
|
-
workbasePermission,
|
|
946
|
-
)
|
|
947
|
-
})
|
|
948
|
-
|
|
949
|
-
test("does not override customized OpenCode access policy", async () => {
|
|
950
|
-
const harness = createHarness({ opencodeIntegrationState: "customized" })
|
|
951
|
-
await harness.run({ taskId: "example", opencode: true })
|
|
952
|
-
|
|
953
|
-
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
954
939
|
expect(
|
|
955
940
|
harness.launchEnvironments[0]?.OPENCODE_CONFIG_CONTENT,
|
|
956
941
|
).toBeUndefined()
|
package/src/commands/work.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
|
-
import { dirname, isAbsolute,
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
|
|
3
3
|
import type { BaseCommandOptions } from "../utils/command"
|
|
4
4
|
import { WorktreeService } from "../services/WorktreeService"
|
|
5
5
|
import { FileSystemService } from "../services/FileSystemService"
|
|
@@ -122,10 +122,7 @@ export const work = (
|
|
|
122
122
|
const inputAllowed = options.inputAllowed ?? true
|
|
123
123
|
const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
|
|
124
124
|
if (!root) return
|
|
125
|
-
|
|
126
|
-
const managedOpencode = integration.files.some(
|
|
127
|
-
(file) => file.name === "opencode" && file.state === "managed",
|
|
128
|
-
)
|
|
125
|
+
yield* integrations.sync(root)
|
|
129
126
|
const { config } = yield* workbase.loadConfig(root)
|
|
130
127
|
|
|
131
128
|
let target: WorkTarget | null = null
|
|
@@ -337,13 +334,6 @@ export const work = (
|
|
|
337
334
|
...runnerEnvironment(runner, variables),
|
|
338
335
|
}
|
|
339
336
|
if (writablePath) environment.AGENCY_WRITABLE_CHECKOUT = writablePath
|
|
340
|
-
if (runner === "opencode" && managedOpencode) {
|
|
341
|
-
environment.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
342
|
-
permission: {
|
|
343
|
-
external_directory: { [join(root, "*")]: "allow" },
|
|
344
|
-
},
|
|
345
|
-
})
|
|
346
|
-
}
|
|
347
337
|
if (options.printCommand) {
|
|
348
338
|
log(
|
|
349
339
|
JSON.stringify(
|
|
@@ -490,8 +480,8 @@ Usage: agency work [<directory-or-task-id> | --epic <epic-id>] [--runner <name>]
|
|
|
490
480
|
Launch an agent for an epic, task, or phase. With no directory, select one
|
|
491
481
|
interactively. A positional argument resolves as a directory first, then as a task
|
|
492
482
|
ID. Use '.' for the current directory. Outside a workbase, select a registered
|
|
493
|
-
workbase first. OpenCode launches receive whole-workbase
|
|
494
|
-
|
|
483
|
+
workbase first. Managed OpenCode launches receive whole-workbase access through
|
|
484
|
+
Agency's project plugin; Agency context remains authoritative for writes.
|
|
495
485
|
|
|
496
486
|
The prepare subcommand resolves and materializes an execution workspace without
|
|
497
487
|
launching an agent or changing lifecycle status. --dry-run reports planned Git
|
|
@@ -113,7 +113,7 @@ describe("IntegrationService", () => {
|
|
|
113
113
|
])
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
-
test("generates a dynamic
|
|
116
|
+
test("generates a dynamic workbase plugin", () => {
|
|
117
117
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
118
118
|
"process.env.AGENCY_WRITABLE_CHECKOUT",
|
|
119
119
|
)
|
|
@@ -129,6 +129,9 @@ describe("IntegrationService", () => {
|
|
|
129
129
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
130
130
|
'join(checkout, ".opencode", "skills")',
|
|
131
131
|
)
|
|
132
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
133
|
+
'config.permission.external_directory = { [join(root, "*")]: "allow" }',
|
|
134
|
+
)
|
|
132
135
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
133
136
|
"config.skills.paths = [...new Set",
|
|
134
137
|
)
|
|
@@ -97,7 +97,7 @@ const describe = (
|
|
|
97
97
|
return state === "managed"
|
|
98
98
|
? {
|
|
99
99
|
diagnostic:
|
|
100
|
-
"Agency's managed OpenCode plugin exposes writable-checkout skills
|
|
100
|
+
"Agency's managed OpenCode plugin provides whole-workbase access and exposes writable-checkout skills.",
|
|
101
101
|
remediation: null,
|
|
102
102
|
}
|
|
103
103
|
: state === "customized"
|
|
@@ -108,9 +108,9 @@ const describe = (
|
|
|
108
108
|
}
|
|
109
109
|
: {
|
|
110
110
|
diagnostic:
|
|
111
|
-
"The managed OpenCode
|
|
111
|
+
"The managed OpenCode workbase plugin needs synchronization.",
|
|
112
112
|
remediation:
|
|
113
|
-
"Run 'agency integration sync' to expose writable-checkout skills in OpenCode.",
|
|
113
|
+
"Run 'agency integration sync' to provide workbase access and expose writable-checkout skills in OpenCode.",
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
if (name === "opencode-tui") {
|
|
@@ -10,7 +10,7 @@ const body = `import { existsSync } from "node:fs"
|
|
|
10
10
|
import { join, sep } from "node:path"
|
|
11
11
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const agencyContext = async (directory: string) => {
|
|
14
14
|
const task = process.env.AGENCY_TASK_ID
|
|
15
15
|
const phase = process.env.AGENCY_PHASE_ID
|
|
16
16
|
const args = task
|
|
@@ -21,14 +21,39 @@ const contextCheckout = async (directory: string) => {
|
|
|
21
21
|
if ((await child.exited) !== 0) return
|
|
22
22
|
const envelope = JSON.parse(output)
|
|
23
23
|
if (envelope.ok !== true) return
|
|
24
|
-
return
|
|
24
|
+
return {
|
|
25
|
+
root: envelope.result?.workbase?.root as string | undefined,
|
|
26
|
+
checkout: envelope.result?.authority?.writable?.checkoutPath as string | undefined,
|
|
27
|
+
}
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const plugin: Plugin = async ({ directory }) => ({
|
|
28
31
|
config: async (config) => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const context = await agencyContext(directory).catch(() => undefined)
|
|
33
|
+
const root = process.env.AGENCY_WORKBASE ?? context?.root
|
|
34
|
+
const checkout = process.env.AGENCY_WRITABLE_CHECKOUT ?? context?.checkout
|
|
35
|
+
|
|
36
|
+
const reference = config.references?.workbase
|
|
37
|
+
if (
|
|
38
|
+
root &&
|
|
39
|
+
typeof reference === "object" &&
|
|
40
|
+
reference.path === ".." &&
|
|
41
|
+
reference.description ===
|
|
42
|
+
"Complete Agency workbase context; write authority still comes only from agency context" &&
|
|
43
|
+
typeof config.permission !== "string"
|
|
44
|
+
) {
|
|
45
|
+
config.permission ??= {}
|
|
46
|
+
const external = config.permission.external_directory
|
|
47
|
+
if (external === undefined) {
|
|
48
|
+
config.permission.external_directory = { [join(root, "*")]: "allow" }
|
|
49
|
+
} else if (typeof external === "object") {
|
|
50
|
+
config.permission.external_directory = {
|
|
51
|
+
[join(root, "*")]: "allow",
|
|
52
|
+
...external,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
if (!checkout) return
|
|
33
58
|
|
|
34
59
|
const paths = [
|