@markjaquith/agency 2.44.0 → 2.46.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 +23 -7
- package/package.json +1 -1
- package/src/cli.test.ts +126 -11
- package/src/commands/init.test.ts +16 -0
- package/src/commands/integration.test.ts +58 -2
- package/src/commands/integration.ts +12 -2
- package/src/commands/work.test.ts +17 -0
- package/src/commands/work.ts +3 -0
- package/src/services/IntegrationService.test.ts +198 -0
- package/src/services/IntegrationService.ts +164 -2
- package/src/workbase/AGENTS.md +9 -4
- package/src/workbase/opencode-plugin-file.ts +61 -0
- package/src/workbase/opencode-tui-file.ts +30 -0
- package/src/workbase/opencode-tui-plugin-file.ts +65 -0
package/README.md
CHANGED
|
@@ -52,6 +52,10 @@ workbase/
|
|
|
52
52
|
AGENTS.md # managed Agency instructions
|
|
53
53
|
.opencode/
|
|
54
54
|
opencode.jsonc # managed @agency subagent, instructions, and reference
|
|
55
|
+
tui.jsonc # managed TUI plugin registration
|
|
56
|
+
command/agency.md # managed /agency workflow command
|
|
57
|
+
plugin/agency-repository-skills.ts # managed checkout skill discovery
|
|
58
|
+
tui/agency-debug.ts # managed /agency-debug TUI diagnostic
|
|
55
59
|
agency.json # tracked config and portable repository declarations
|
|
56
60
|
repos/ # ignored local materializations
|
|
57
61
|
frontend/ # bare Git repository or symlink
|
|
@@ -80,9 +84,10 @@ workbase/
|
|
|
80
84
|
|
|
81
85
|
Agency keeps discovery and other observational commands read-only. Run
|
|
82
86
|
`agency integration status` to inspect `.agency/AGENTS.md` and
|
|
83
|
-
`.opencode/opencode.jsonc`,
|
|
84
|
-
integration sync` to create missing files or refresh
|
|
85
|
-
files. Customized files are reported but never
|
|
87
|
+
`.opencode/opencode.jsonc`, `.opencode/tui.jsonc`, and their managed command and
|
|
88
|
+
plugin files, then `agency integration sync` to create missing files or refresh
|
|
89
|
+
checksum-safe managed files. Customized files are reported but never
|
|
90
|
+
overwritten. The root
|
|
86
91
|
`AGENTS.md` is user-owned and is not inspected or modified by Agency.
|
|
87
92
|
|
|
88
93
|
When upgrading an existing workbase, synchronization moves a checksum-valid
|
|
@@ -97,6 +102,11 @@ and replaces the built-in Plan agent with `agency-plan`. That planning agent can
|
|
|
97
102
|
update `TASK.md`, `PHASE.md`, and `EPIC.md` while other edits remain disabled.
|
|
98
103
|
When the subagent launches work in another agent, it verifies that the runner
|
|
99
104
|
started and returns without waiting for the task to finish.
|
|
105
|
+
The TUI-only `/agency-debug` command reports TUI companion initialization and
|
|
106
|
+
whether the server plugin registered writable-checkout skills. It uses a native
|
|
107
|
+
toast and does not submit a prompt to an LLM. When no writable checkout skill
|
|
108
|
+
directory is available, server initialization is reported as indeterminate
|
|
109
|
+
rather than inferred from plugin discovery.
|
|
100
110
|
OpenCode discovers the config from task and epic launch directories. Agents
|
|
101
111
|
receive whole-workbase visibility from that reference. Bash and Agency operations
|
|
102
112
|
must still follow the write authority reported by `agency context`.
|
|
@@ -232,12 +242,18 @@ Every runner receives the same `AGENCY_RUNNER`, `AGENCY_CLAIMANT`,
|
|
|
232
242
|
`AGENCY_SESSION_ID`, `AGENCY_CLAIM_REVISION`, `AGENCY_WORKBASE`, `AGENCY_TARGET`,
|
|
233
243
|
`AGENCY_TASK_ID`, `AGENCY_PHASE_ID`, and `AGENCY_PROMPT` environment. Configured
|
|
234
244
|
environment is added without overriding these normalized values.
|
|
245
|
+
Execution-unit runners also receive `AGENCY_WRITABLE_CHECKOUT` with the
|
|
246
|
+
authoritative writable checkout path.
|
|
235
247
|
`AGENCY_CLAIM_REVISION` is empty for local `agency work` launches.
|
|
236
248
|
`AGENCY_PROMPT` is empty unless `--auto` is set.
|
|
237
|
-
The `opencode` runner
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
`
|
|
249
|
+
The `opencode` runner remains rooted in its task or epic working directory so
|
|
250
|
+
the workbase `AGENTS.md` and managed OpenCode config are discovered normally.
|
|
251
|
+
Agency's managed OpenCode plugin adds existing checkout-local `.claude/skills`,
|
|
252
|
+
`.agents/skills`, and `.opencode/{skill,skills}` directories to `skills.paths`.
|
|
253
|
+
`agency work` supplies the checkout directly; plain OpenCode launches resolve a
|
|
254
|
+
materialized execution-unit checkout through `agency context`. A multi-phase
|
|
255
|
+
task root has no single checkout, so launch from its phase directory when using
|
|
256
|
+
plain OpenCode. Other checkout-local configuration is not composed.
|
|
241
257
|
`--print-command` prints the exact cwd and argv plus non-secret environment keys
|
|
242
258
|
without launching the runner.
|
|
243
259
|
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterAll, afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { access, mkdir, realpath } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
2
|
+
import { access, mkdir, realpath, symlink } from "node:fs/promises"
|
|
3
|
+
import { join, sep } from "node:path"
|
|
4
4
|
import errorFixture from "../fixtures/protocol/error.json"
|
|
5
5
|
import successFixture from "../fixtures/protocol/success.json"
|
|
6
6
|
import { cleanupTempDir, createTempDir } from "./test-utils"
|
|
@@ -527,6 +527,9 @@ describe("CLI", () => {
|
|
|
527
527
|
{ name: "agents", state: "managed" },
|
|
528
528
|
{ name: "opencode", state: "managed" },
|
|
529
529
|
{ name: "opencode-command", state: "managed" },
|
|
530
|
+
{ name: "opencode-plugin", state: "managed" },
|
|
531
|
+
{ name: "opencode-tui", state: "managed" },
|
|
532
|
+
{ name: "opencode-tui-plugin", state: "managed" },
|
|
530
533
|
])
|
|
531
534
|
|
|
532
535
|
const synced = parseJson(
|
|
@@ -536,6 +539,9 @@ describe("CLI", () => {
|
|
|
536
539
|
{ name: "agents", state: "managed", changed: false },
|
|
537
540
|
{ name: "opencode", state: "managed", changed: false },
|
|
538
541
|
{ name: "opencode-command", state: "managed", changed: false },
|
|
542
|
+
{ name: "opencode-plugin", state: "managed", changed: false },
|
|
543
|
+
{ name: "opencode-tui", state: "managed", changed: false },
|
|
544
|
+
{ name: "opencode-tui-plugin", state: "managed", changed: false },
|
|
539
545
|
])
|
|
540
546
|
})
|
|
541
547
|
|
|
@@ -864,10 +870,17 @@ status: open
|
|
|
864
870
|
.exitCode,
|
|
865
871
|
).toBe(0)
|
|
866
872
|
await Bun.write(join(source, "README.md"), "example\n")
|
|
873
|
+
await mkdir(join(source, ".claude/skills/repository-skill"), {
|
|
874
|
+
recursive: true,
|
|
875
|
+
})
|
|
876
|
+
await Bun.write(
|
|
877
|
+
join(source, ".claude/skills/repository-skill/SKILL.md"),
|
|
878
|
+
"---\nname: repository-skill\ndescription: Repository discovery test.\n---\n\nRepository skill content.\n",
|
|
879
|
+
)
|
|
867
880
|
for (const args of [
|
|
868
881
|
["config", "user.email", "test@example.com"],
|
|
869
882
|
["config", "user.name", "Test"],
|
|
870
|
-
["add", "
|
|
883
|
+
["add", "."],
|
|
871
884
|
["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
|
|
872
885
|
]) {
|
|
873
886
|
expect(Bun.spawnSync(["git", "-C", source, ...args]).exitCode).toBe(0)
|
|
@@ -1001,18 +1014,35 @@ status: open
|
|
|
1001
1014
|
{
|
|
1002
1015
|
args: ["--task", "example"],
|
|
1003
1016
|
cwd: join(workbaseRoot, "tasks/example"),
|
|
1017
|
+
checkoutPath: join(workbaseRoot, "tasks/example/code/agency"),
|
|
1018
|
+
skillPath: join(
|
|
1019
|
+
workbaseRoot,
|
|
1020
|
+
"tasks/example/code/agency/.claude/skills",
|
|
1021
|
+
),
|
|
1004
1022
|
},
|
|
1005
1023
|
{
|
|
1006
1024
|
args: ["--task", "pipeline", "--phase", "build"],
|
|
1007
1025
|
cwd: join(workbaseRoot, "tasks/pipeline"),
|
|
1026
|
+
checkoutPath: join(
|
|
1027
|
+
workbaseRoot,
|
|
1028
|
+
"tasks/pipeline/phases/build/code/agency",
|
|
1029
|
+
),
|
|
1030
|
+
skillPath: join(
|
|
1031
|
+
workbaseRoot,
|
|
1032
|
+
"tasks/pipeline/phases/build/code/agency/.claude/skills",
|
|
1033
|
+
),
|
|
1008
1034
|
},
|
|
1009
1035
|
{
|
|
1010
1036
|
args: ["--epic", "delivery"],
|
|
1011
1037
|
cwd: join(workbaseRoot, "epics/delivery"),
|
|
1038
|
+
checkoutPath: undefined,
|
|
1039
|
+
skillPath: undefined,
|
|
1012
1040
|
},
|
|
1013
1041
|
{
|
|
1014
1042
|
args: ["--task", "pipeline"],
|
|
1015
1043
|
cwd: join(workbaseRoot, "tasks/pipeline"),
|
|
1044
|
+
checkoutPath: undefined,
|
|
1045
|
+
skillPath: undefined,
|
|
1016
1046
|
},
|
|
1017
1047
|
]
|
|
1018
1048
|
for (const launch of launches) {
|
|
@@ -1024,7 +1054,11 @@ status: open
|
|
|
1024
1054
|
expect(printed.stderr).toBe("")
|
|
1025
1055
|
const contract = JSON.parse(printed.stdout)
|
|
1026
1056
|
expect(contract.cwd).toBe(launch.cwd)
|
|
1057
|
+
expect(contract.argv).toEqual(["opencode"])
|
|
1027
1058
|
expect(contract.environment.OPENCODE_CONFIG).toBeUndefined()
|
|
1059
|
+
expect(contract.environment.AGENCY_WRITABLE_CHECKOUT).toBe(
|
|
1060
|
+
launch.checkoutPath,
|
|
1061
|
+
)
|
|
1028
1062
|
expect(
|
|
1029
1063
|
JSON.parse(contract.environment.OPENCODE_CONFIG_CONTENT),
|
|
1030
1064
|
).toEqual({
|
|
@@ -1053,13 +1087,49 @@ status: open
|
|
|
1053
1087
|
}),
|
|
1054
1088
|
]),
|
|
1055
1089
|
)
|
|
1090
|
+
const configProbe = Bun.spawnSync(["opencode", "debug", "config"], {
|
|
1091
|
+
cwd: contract.cwd,
|
|
1092
|
+
env: environment,
|
|
1093
|
+
})
|
|
1094
|
+
expect(configProbe.exitCode).toBe(0)
|
|
1095
|
+
const effectiveConfig = JSON.parse(configProbe.stdout.toString())
|
|
1096
|
+
expect(effectiveConfig.plugin_origins).toEqual(
|
|
1097
|
+
expect.arrayContaining([
|
|
1098
|
+
expect.objectContaining({
|
|
1099
|
+
spec: expect.stringContaining("agency-repository-skills.ts"),
|
|
1100
|
+
}),
|
|
1101
|
+
]),
|
|
1102
|
+
)
|
|
1103
|
+
if (launch.skillPath) {
|
|
1104
|
+
expect(effectiveConfig.skills.paths).toContain(
|
|
1105
|
+
`${launch.skillPath}${sep}.`,
|
|
1106
|
+
)
|
|
1107
|
+
const skillProbe = Bun.spawnSync(
|
|
1108
|
+
[
|
|
1109
|
+
"opencode",
|
|
1110
|
+
"debug",
|
|
1111
|
+
"agent",
|
|
1112
|
+
"build",
|
|
1113
|
+
"--tool",
|
|
1114
|
+
"skill",
|
|
1115
|
+
"--params",
|
|
1116
|
+
JSON.stringify({ name: "repository-skill" }),
|
|
1117
|
+
],
|
|
1118
|
+
{ cwd: contract.cwd, env: environment },
|
|
1119
|
+
)
|
|
1120
|
+
expect(skillProbe.exitCode).toBe(0)
|
|
1121
|
+
expect(
|
|
1122
|
+
JSON.parse(skillProbe.stdout.toString()).result.output,
|
|
1123
|
+
).toContain("Repository skill content.")
|
|
1124
|
+
} else {
|
|
1125
|
+
expect(
|
|
1126
|
+
(effectiveConfig.skills?.paths ?? []).filter((path: string) =>
|
|
1127
|
+
path.endsWith(`${sep}.`),
|
|
1128
|
+
),
|
|
1129
|
+
).toEqual([])
|
|
1130
|
+
}
|
|
1056
1131
|
if (launch === launches[0]) {
|
|
1057
|
-
|
|
1058
|
-
cwd: contract.cwd,
|
|
1059
|
-
env: environment,
|
|
1060
|
-
})
|
|
1061
|
-
expect(configProbe.exitCode).toBe(0)
|
|
1062
|
-
const effectiveConfig = JSON.parse(configProbe.stdout.toString())
|
|
1132
|
+
expect(effectiveConfig.instructions).toContain(".agency/AGENTS.md")
|
|
1063
1133
|
expect(effectiveConfig.agent.agency).toMatchObject({
|
|
1064
1134
|
description: expect.stringContaining(
|
|
1065
1135
|
"Agency workbase orchestration",
|
|
@@ -1106,19 +1176,28 @@ status: open
|
|
|
1106
1176
|
}
|
|
1107
1177
|
}
|
|
1108
1178
|
|
|
1179
|
+
const agencyBin = join(parent, "bin")
|
|
1180
|
+
await mkdir(agencyBin)
|
|
1181
|
+
await symlink(cliPath, join(agencyBin, "agency"))
|
|
1109
1182
|
const directEnvironment: Record<string, string | undefined> = {
|
|
1110
1183
|
...process.env,
|
|
1184
|
+
PATH: `${agencyBin}:${process.env.PATH ?? ""}`,
|
|
1111
1185
|
XDG_CONFIG_HOME: isolatedConfigHome,
|
|
1112
1186
|
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
1113
1187
|
}
|
|
1114
1188
|
delete directEnvironment.OPENCODE_CONFIG
|
|
1115
1189
|
delete directEnvironment.OPENCODE_CONFIG_CONTENT
|
|
1116
1190
|
delete directEnvironment.AGENCY_WORKBASE
|
|
1117
|
-
|
|
1191
|
+
delete directEnvironment.AGENCY_TASK_ID
|
|
1192
|
+
delete directEnvironment.AGENCY_PHASE_ID
|
|
1193
|
+
delete directEnvironment.AGENCY_WRITABLE_CHECKOUT
|
|
1194
|
+
const directDirectories = [
|
|
1118
1195
|
join(workbaseRoot, "tasks/example"),
|
|
1119
1196
|
join(workbaseRoot, "tasks/pipeline"),
|
|
1197
|
+
join(workbaseRoot, "tasks/pipeline/phases/build"),
|
|
1120
1198
|
join(workbaseRoot, "epics/delivery"),
|
|
1121
|
-
]
|
|
1199
|
+
]
|
|
1200
|
+
for (const cwd of directDirectories) {
|
|
1122
1201
|
const probe = Bun.spawnSync(["opencode", "debug", "agent", "build"], {
|
|
1123
1202
|
cwd,
|
|
1124
1203
|
env: directEnvironment,
|
|
@@ -1134,6 +1213,20 @@ status: open
|
|
|
1134
1213
|
}),
|
|
1135
1214
|
]),
|
|
1136
1215
|
)
|
|
1216
|
+
const configProbe = Bun.spawnSync(["opencode", "debug", "config"], {
|
|
1217
|
+
cwd,
|
|
1218
|
+
env: directEnvironment,
|
|
1219
|
+
})
|
|
1220
|
+
expect(configProbe.exitCode).toBe(0)
|
|
1221
|
+
expect(
|
|
1222
|
+
JSON.parse(configProbe.stdout.toString()).plugin_origins,
|
|
1223
|
+
).toEqual(
|
|
1224
|
+
expect.arrayContaining([
|
|
1225
|
+
expect.objectContaining({
|
|
1226
|
+
spec: expect.stringContaining("agency-repository-skills.ts"),
|
|
1227
|
+
}),
|
|
1228
|
+
]),
|
|
1229
|
+
)
|
|
1137
1230
|
for (const document of documents) {
|
|
1138
1231
|
const read = Bun.spawnSync(
|
|
1139
1232
|
[
|
|
@@ -1154,6 +1247,28 @@ status: open
|
|
|
1154
1247
|
)
|
|
1155
1248
|
}
|
|
1156
1249
|
}
|
|
1250
|
+
for (const cwd of [
|
|
1251
|
+
join(workbaseRoot, "tasks/example"),
|
|
1252
|
+
join(workbaseRoot, "tasks/pipeline/phases/build"),
|
|
1253
|
+
]) {
|
|
1254
|
+
const manualSkill = Bun.spawnSync(
|
|
1255
|
+
[
|
|
1256
|
+
"opencode",
|
|
1257
|
+
"debug",
|
|
1258
|
+
"agent",
|
|
1259
|
+
"build",
|
|
1260
|
+
"--tool",
|
|
1261
|
+
"skill",
|
|
1262
|
+
"--params",
|
|
1263
|
+
JSON.stringify({ name: "repository-skill" }),
|
|
1264
|
+
],
|
|
1265
|
+
{ cwd, env: directEnvironment },
|
|
1266
|
+
)
|
|
1267
|
+
expect(manualSkill.exitCode, manualSkill.stderr.toString()).toBe(0)
|
|
1268
|
+
expect(
|
|
1269
|
+
JSON.parse(manualSkill.stdout.toString()).result.output,
|
|
1270
|
+
).toContain("Repository skill content.")
|
|
1271
|
+
}
|
|
1157
1272
|
|
|
1158
1273
|
parseJson(
|
|
1159
1274
|
await runCli(["task", "status", "example", "dropped", "--json"], root),
|
|
@@ -66,6 +66,22 @@ describe("init command", () => {
|
|
|
66
66
|
).text()
|
|
67
67
|
expect(command).toContain("Workflow: `$1`")
|
|
68
68
|
expect(command).toContain("Optional target: `$2`")
|
|
69
|
+
const plugin = await Bun.file(
|
|
70
|
+
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
71
|
+
).text()
|
|
72
|
+
expect(plugin).toContain("AGENCY_WRITABLE_CHECKOUT")
|
|
73
|
+
expect(plugin).toContain("config.skills.paths")
|
|
74
|
+
const tui = await Bun.file(join(root, ".opencode/tui.jsonc")).text()
|
|
75
|
+
expect(JSON.parse(tui.slice(tui.indexOf("\n\n") + 2))).toEqual({
|
|
76
|
+
$schema: "https://opencode.ai/tui.json",
|
|
77
|
+
plugin: ["./tui/agency-debug.ts"],
|
|
78
|
+
})
|
|
79
|
+
const tuiPlugin = await Bun.file(
|
|
80
|
+
join(root, ".opencode/tui/agency-debug.ts"),
|
|
81
|
+
).text()
|
|
82
|
+
expect(tuiPlugin).toContain('slashName: "agency-debug"')
|
|
83
|
+
expect(tuiPlugin).toContain("api.ui.toast")
|
|
84
|
+
expect(tuiPlugin).not.toContain("chat.message")
|
|
69
85
|
})
|
|
70
86
|
|
|
71
87
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -41,6 +41,21 @@ describe("integration command", () => {
|
|
|
41
41
|
state: "missing",
|
|
42
42
|
remediation: expect.stringContaining("integration sync"),
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: "opencode-plugin",
|
|
46
|
+
state: "missing",
|
|
47
|
+
remediation: expect.stringContaining("integration sync"),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "opencode-tui",
|
|
51
|
+
state: "missing",
|
|
52
|
+
remediation: expect.stringContaining("integration sync"),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "opencode-tui-plugin",
|
|
56
|
+
state: "missing",
|
|
57
|
+
remediation: expect.stringContaining("integration sync"),
|
|
58
|
+
},
|
|
44
59
|
],
|
|
45
60
|
})
|
|
46
61
|
})
|
|
@@ -79,7 +94,22 @@ OpenCode config: missing
|
|
|
79
94
|
OpenCode /agency command: missing
|
|
80
95
|
Path: .opencode/command/agency.md
|
|
81
96
|
The managed OpenCode /agency command needs synchronization.
|
|
82
|
-
Action: Run 'agency integration sync' to install the managed /agency command
|
|
97
|
+
Action: Run 'agency integration sync' to install the managed /agency command.
|
|
98
|
+
|
|
99
|
+
OpenCode checkout skills: missing
|
|
100
|
+
Path: .opencode/plugin/agency-repository-skills.ts
|
|
101
|
+
The managed OpenCode checkout-skill plugin needs synchronization.
|
|
102
|
+
Action: Run 'agency integration sync' to expose writable-checkout skills in OpenCode.
|
|
103
|
+
|
|
104
|
+
OpenCode TUI config: missing
|
|
105
|
+
Path: .opencode/tui.jsonc
|
|
106
|
+
The managed OpenCode TUI config needs synchronization.
|
|
107
|
+
Action: Run 'agency integration sync' to register /agency-debug.
|
|
108
|
+
|
|
109
|
+
OpenCode /agency-debug: missing
|
|
110
|
+
Path: .opencode/tui/agency-debug.ts
|
|
111
|
+
The managed OpenCode TUI diagnostic companion needs synchronization.
|
|
112
|
+
Action: Run 'agency integration sync' to install /agency-debug.`)
|
|
83
113
|
})
|
|
84
114
|
|
|
85
115
|
test("explicitly synchronizes integration files", async () => {
|
|
@@ -91,6 +121,9 @@ OpenCode /agency command: missing
|
|
|
91
121
|
{ name: "agents", state: "managed", changed: true },
|
|
92
122
|
{ name: "opencode", state: "managed", changed: true },
|
|
93
123
|
{ name: "opencode-command", state: "managed", changed: true },
|
|
124
|
+
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
125
|
+
{ name: "opencode-tui", state: "managed", changed: true },
|
|
126
|
+
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
94
127
|
])
|
|
95
128
|
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
96
129
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(true)
|
|
@@ -100,6 +133,17 @@ OpenCode /agency command: missing
|
|
|
100
133
|
expect(
|
|
101
134
|
await Bun.file(join(root, ".opencode/command/agency.md")).exists(),
|
|
102
135
|
).toBe(true)
|
|
136
|
+
expect(
|
|
137
|
+
await Bun.file(
|
|
138
|
+
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
139
|
+
).exists(),
|
|
140
|
+
).toBe(true)
|
|
141
|
+
expect(await Bun.file(join(root, ".opencode/tui.jsonc")).exists()).toBe(
|
|
142
|
+
true,
|
|
143
|
+
)
|
|
144
|
+
expect(
|
|
145
|
+
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).exists(),
|
|
146
|
+
).toBe(true)
|
|
103
147
|
})
|
|
104
148
|
|
|
105
149
|
test("formats integration sync results for people", async () => {
|
|
@@ -119,6 +163,18 @@ OpenCode config: synced
|
|
|
119
163
|
|
|
120
164
|
OpenCode /agency command: synced
|
|
121
165
|
Path: .opencode/command/agency.md
|
|
122
|
-
Agency's managed OpenCode /agency command is current
|
|
166
|
+
Agency's managed OpenCode /agency command is current.
|
|
167
|
+
|
|
168
|
+
OpenCode checkout skills: synced
|
|
169
|
+
Path: .opencode/plugin/agency-repository-skills.ts
|
|
170
|
+
Agency's managed OpenCode plugin exposes writable-checkout skills to work-item sessions.
|
|
171
|
+
|
|
172
|
+
OpenCode TUI config: synced
|
|
173
|
+
Path: .opencode/tui.jsonc
|
|
174
|
+
Agency's managed OpenCode TUI config explicitly loads /agency-debug.
|
|
175
|
+
|
|
176
|
+
OpenCode /agency-debug: synced
|
|
177
|
+
Path: .opencode/tui/agency-debug.ts
|
|
178
|
+
Agency's managed OpenCode TUI diagnostic companion is current.`)
|
|
123
179
|
})
|
|
124
180
|
})
|
|
@@ -12,7 +12,13 @@ interface IntegrationOptions extends BaseCommandOptions {
|
|
|
12
12
|
interface IntegrationResult {
|
|
13
13
|
readonly root: string
|
|
14
14
|
readonly files: readonly {
|
|
15
|
-
readonly name:
|
|
15
|
+
readonly name:
|
|
16
|
+
| "agents"
|
|
17
|
+
| "opencode"
|
|
18
|
+
| "opencode-command"
|
|
19
|
+
| "opencode-plugin"
|
|
20
|
+
| "opencode-tui"
|
|
21
|
+
| "opencode-tui-plugin"
|
|
16
22
|
readonly path: string
|
|
17
23
|
readonly state: string
|
|
18
24
|
readonly diagnostic: string
|
|
@@ -25,6 +31,9 @@ const integrationNames = {
|
|
|
25
31
|
agents: "Agent instructions",
|
|
26
32
|
opencode: "OpenCode config",
|
|
27
33
|
"opencode-command": "OpenCode /agency command",
|
|
34
|
+
"opencode-plugin": "OpenCode checkout skills",
|
|
35
|
+
"opencode-tui": "OpenCode TUI config",
|
|
36
|
+
"opencode-tui-plugin": "OpenCode /agency-debug",
|
|
28
37
|
} as const
|
|
29
38
|
|
|
30
39
|
const logHumanResult = (
|
|
@@ -83,7 +92,8 @@ Usage: agency integration <subcommand>
|
|
|
83
92
|
|
|
84
93
|
Inspect or explicitly synchronize managed agent integration files. OpenCode
|
|
85
94
|
launches load the managed instructions and project config at runtime, and expose
|
|
86
|
-
the managed /agency command,
|
|
95
|
+
the managed /agency command, writable-checkout skills, and /agency-debug TUI
|
|
96
|
+
diagnostic, without changing Agency write authority.
|
|
87
97
|
|
|
88
98
|
Subcommands:
|
|
89
99
|
status Report file state, access diagnostics, and safe remediation
|
|
@@ -929,6 +929,23 @@ describe("work command", () => {
|
|
|
929
929
|
expect(printed.environment.OPENCODE_CONFIG_CONTENT).toBe(workbasePermission)
|
|
930
930
|
})
|
|
931
931
|
|
|
932
|
+
test("provides the writable checkout to plugins without changing the project", async () => {
|
|
933
|
+
const harness = createHarness()
|
|
934
|
+
await harness.run({ taskId: "example", opencode: true })
|
|
935
|
+
|
|
936
|
+
expect(harness.launches[0]).toEqual({
|
|
937
|
+
cli: "opencode",
|
|
938
|
+
args: ["opencode"],
|
|
939
|
+
cwd: taskDirectory,
|
|
940
|
+
})
|
|
941
|
+
expect(harness.launchEnvironments[0]?.AGENCY_WRITABLE_CHECKOUT).toBe(
|
|
942
|
+
"/workbase/tasks/example/code/agency",
|
|
943
|
+
)
|
|
944
|
+
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG_CONTENT).toBe(
|
|
945
|
+
workbasePermission,
|
|
946
|
+
)
|
|
947
|
+
})
|
|
948
|
+
|
|
932
949
|
test("does not override customized OpenCode access policy", async () => {
|
|
933
950
|
const harness = createHarness({ opencodeIntegrationState: "customized" })
|
|
934
951
|
await harness.run({ taskId: "example", opencode: true })
|
package/src/commands/work.ts
CHANGED
|
@@ -239,6 +239,7 @@ export const work = (
|
|
|
239
239
|
(target.status !== undefined && target.status !== "open"))
|
|
240
240
|
let prompt: string
|
|
241
241
|
let launchPath: string
|
|
242
|
+
let writablePath: string | undefined
|
|
242
243
|
if (target.kind === "epic") {
|
|
243
244
|
prompt = `Work on the epic. Read ${target.path}.`
|
|
244
245
|
launchPath = dirname(target.path)
|
|
@@ -264,6 +265,7 @@ export const work = (
|
|
|
264
265
|
? `${action} the task. Read ${workspace.taskPath} and ${workspace.phasePath}.`
|
|
265
266
|
: `${action} the task. Read ${workspace.taskPath}.`
|
|
266
267
|
launchPath = dirname(workspace.taskPath)
|
|
268
|
+
writablePath = workspace.writablePath
|
|
267
269
|
}
|
|
268
270
|
|
|
269
271
|
const explicitlyRequested = Boolean(
|
|
@@ -334,6 +336,7 @@ export const work = (
|
|
|
334
336
|
...resolved.environment,
|
|
335
337
|
...runnerEnvironment(runner, variables),
|
|
336
338
|
}
|
|
339
|
+
if (writablePath) environment.AGENCY_WRITABLE_CHECKOUT = writablePath
|
|
337
340
|
if (runner === "opencode" && managedOpencode) {
|
|
338
341
|
environment.OPENCODE_CONFIG_CONTENT = JSON.stringify({
|
|
339
342
|
permission: {
|
|
@@ -3,6 +3,7 @@ import { Effect } from "effect"
|
|
|
3
3
|
import { createHash } from "node:crypto"
|
|
4
4
|
import { mkdir, stat, symlink, unlink, utimes } from "node:fs/promises"
|
|
5
5
|
import { dirname, join } from "node:path"
|
|
6
|
+
import { pathToFileURL } from "node:url"
|
|
6
7
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
7
8
|
import { managedWorkbaseAgents } from "../workbase/agents-file"
|
|
8
9
|
import {
|
|
@@ -10,6 +11,15 @@ import {
|
|
|
10
11
|
managedWorkbaseOpencodeCommand,
|
|
11
12
|
} from "../workbase/opencode-command-file"
|
|
12
13
|
import { managedWorkbaseOpencode } from "../workbase/opencode-file"
|
|
14
|
+
import {
|
|
15
|
+
canUpdateManagedWorkbaseOpencodePlugin,
|
|
16
|
+
managedWorkbaseOpencodePlugin,
|
|
17
|
+
} from "../workbase/opencode-plugin-file"
|
|
18
|
+
import { managedWorkbaseOpencodeTui } from "../workbase/opencode-tui-file"
|
|
19
|
+
import {
|
|
20
|
+
canUpdateManagedWorkbaseOpencodeTuiPlugin,
|
|
21
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
22
|
+
} from "../workbase/opencode-tui-plugin-file"
|
|
13
23
|
import { IntegrationService } from "./IntegrationService"
|
|
14
24
|
|
|
15
25
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -51,6 +61,9 @@ describe("IntegrationService", () => {
|
|
|
51
61
|
"missing",
|
|
52
62
|
"missing",
|
|
53
63
|
"missing",
|
|
64
|
+
"missing",
|
|
65
|
+
"missing",
|
|
66
|
+
"missing",
|
|
54
67
|
])
|
|
55
68
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
56
69
|
|
|
@@ -61,10 +74,24 @@ describe("IntegrationService", () => {
|
|
|
61
74
|
".opencode/command/agency.md",
|
|
62
75
|
managedWorkbaseOpencodeCommand,
|
|
63
76
|
)
|
|
77
|
+
await write(
|
|
78
|
+
root,
|
|
79
|
+
".opencode/plugin/agency-repository-skills.ts",
|
|
80
|
+
managedWorkbaseOpencodePlugin,
|
|
81
|
+
)
|
|
82
|
+
await write(root, ".opencode/tui.jsonc", managedWorkbaseOpencodeTui)
|
|
83
|
+
await write(
|
|
84
|
+
root,
|
|
85
|
+
".opencode/tui/agency-debug.ts",
|
|
86
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
87
|
+
)
|
|
64
88
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
65
89
|
"managed",
|
|
66
90
|
"managed",
|
|
67
91
|
"managed",
|
|
92
|
+
"managed",
|
|
93
|
+
"managed",
|
|
94
|
+
"managed",
|
|
68
95
|
])
|
|
69
96
|
})
|
|
70
97
|
|
|
@@ -80,9 +107,111 @@ describe("IntegrationService", () => {
|
|
|
80
107
|
"customized",
|
|
81
108
|
"drifted",
|
|
82
109
|
"missing",
|
|
110
|
+
"missing",
|
|
111
|
+
"missing",
|
|
112
|
+
"missing",
|
|
83
113
|
])
|
|
84
114
|
})
|
|
85
115
|
|
|
116
|
+
test("generates a dynamic repository skill plugin", () => {
|
|
117
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
118
|
+
"process.env.AGENCY_WRITABLE_CHECKOUT",
|
|
119
|
+
)
|
|
120
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
121
|
+
'["agency", "context", ".", "--compact", "--json"]',
|
|
122
|
+
)
|
|
123
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
124
|
+
'join(checkout, ".claude", "skills")',
|
|
125
|
+
)
|
|
126
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
127
|
+
'join(checkout, ".agents", "skills")',
|
|
128
|
+
)
|
|
129
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
130
|
+
'join(checkout, ".opencode", "skills")',
|
|
131
|
+
)
|
|
132
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
133
|
+
"config.skills.paths = [...new Set",
|
|
134
|
+
)
|
|
135
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
136
|
+
".map((path) => `${path}${sep}.`)",
|
|
137
|
+
)
|
|
138
|
+
expect(
|
|
139
|
+
canUpdateManagedWorkbaseOpencodePlugin(managedWorkbaseOpencodePlugin),
|
|
140
|
+
).toBe(true)
|
|
141
|
+
expect(
|
|
142
|
+
canUpdateManagedWorkbaseOpencodePlugin(
|
|
143
|
+
managedWorkbaseOpencodePlugin.replace("config.skills ??= {}", ""),
|
|
144
|
+
),
|
|
145
|
+
).toBe(false)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("registers a TUI-only /agency-debug diagnostic", async () => {
|
|
149
|
+
const config = JSON.parse(managedBody(managedWorkbaseOpencodeTui))
|
|
150
|
+
expect(config).toEqual({
|
|
151
|
+
$schema: "https://opencode.ai/tui.json",
|
|
152
|
+
plugin: ["./tui/agency-debug.ts"],
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const path = join(root, ".opencode/tui/agency-debug.ts")
|
|
156
|
+
await write(
|
|
157
|
+
root,
|
|
158
|
+
".opencode/tui/agency-debug.ts",
|
|
159
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
160
|
+
)
|
|
161
|
+
const module = await import(pathToFileURL(path).href)
|
|
162
|
+
let clientReads = 0
|
|
163
|
+
|
|
164
|
+
const runDiagnostic = async (paths: string[], ready = true) => {
|
|
165
|
+
let command:
|
|
166
|
+
| {
|
|
167
|
+
slashName?: string
|
|
168
|
+
namespace?: string
|
|
169
|
+
run: () => void
|
|
170
|
+
}
|
|
171
|
+
| undefined
|
|
172
|
+
let toast: { variant: string; message: string } | undefined
|
|
173
|
+
await module.default.tui({
|
|
174
|
+
get client() {
|
|
175
|
+
clientReads += 1
|
|
176
|
+
throw new Error("diagnostic must not access the server client")
|
|
177
|
+
},
|
|
178
|
+
state: { ready, config: { skills: { paths } } },
|
|
179
|
+
keymap: {
|
|
180
|
+
registerLayer: (layer: { commands: (typeof command)[] }) => {
|
|
181
|
+
command = layer.commands[0]
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
ui: {
|
|
185
|
+
toast: (input: { variant: string; message: string }) => {
|
|
186
|
+
toast = input
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
} as never)
|
|
190
|
+
expect(command).toMatchObject({
|
|
191
|
+
namespace: "palette",
|
|
192
|
+
slashName: "agency-debug",
|
|
193
|
+
})
|
|
194
|
+
command?.run()
|
|
195
|
+
return toast
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
expect(await runDiagnostic(["/checkout/.agents/skills/."])).toMatchObject({
|
|
199
|
+
variant: "success",
|
|
200
|
+
message: expect.stringContaining("Server plugin: initialized"),
|
|
201
|
+
})
|
|
202
|
+
expect(await runDiagnostic([])).toMatchObject({
|
|
203
|
+
variant: "warning",
|
|
204
|
+
message: expect.stringContaining("Server plugin: indeterminate"),
|
|
205
|
+
})
|
|
206
|
+
expect(clientReads).toBe(0)
|
|
207
|
+
expect(managedWorkbaseOpencodeTuiPlugin).not.toContain("chat.message")
|
|
208
|
+
expect(
|
|
209
|
+
canUpdateManagedWorkbaseOpencodeTuiPlugin(
|
|
210
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
211
|
+
),
|
|
212
|
+
).toBe(true)
|
|
213
|
+
})
|
|
214
|
+
|
|
86
215
|
test("generates a positional OpenCode command for Agency workflows", () => {
|
|
87
216
|
expect(managedWorkbaseOpencodeCommand).toContain(
|
|
88
217
|
"description: Operate Agency work",
|
|
@@ -247,6 +376,9 @@ describe("IntegrationService", () => {
|
|
|
247
376
|
{ name: "agents", state: "customized", changed: false },
|
|
248
377
|
{ name: "opencode", state: "managed", changed: true },
|
|
249
378
|
{ name: "opencode-command", state: "managed", changed: true },
|
|
379
|
+
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
380
|
+
{ name: "opencode-tui", state: "managed", changed: true },
|
|
381
|
+
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
250
382
|
])
|
|
251
383
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
252
384
|
customRootAgents,
|
|
@@ -260,6 +392,17 @@ describe("IntegrationService", () => {
|
|
|
260
392
|
expect(
|
|
261
393
|
await Bun.file(join(root, ".opencode/command/agency.md")).text(),
|
|
262
394
|
).toBe(managedWorkbaseOpencodeCommand)
|
|
395
|
+
expect(
|
|
396
|
+
await Bun.file(
|
|
397
|
+
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
398
|
+
).text(),
|
|
399
|
+
).toBe(managedWorkbaseOpencodePlugin)
|
|
400
|
+
expect(await Bun.file(join(root, ".opencode/tui.jsonc")).text()).toBe(
|
|
401
|
+
managedWorkbaseOpencodeTui,
|
|
402
|
+
)
|
|
403
|
+
expect(
|
|
404
|
+
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
|
|
405
|
+
).toBe(managedWorkbaseOpencodeTuiPlugin)
|
|
263
406
|
|
|
264
407
|
await unlink(join(root, ".agency/AGENTS.md"))
|
|
265
408
|
const second = await sync(root)
|
|
@@ -272,6 +415,61 @@ describe("IntegrationService", () => {
|
|
|
272
415
|
)
|
|
273
416
|
})
|
|
274
417
|
|
|
418
|
+
test("preserves user-owned checkout-skill plugins at either supported path", async () => {
|
|
419
|
+
const custom = "export default async () => ({})\n"
|
|
420
|
+
await write(root, ".opencode/plugins/agency-repository-skills.ts", custom)
|
|
421
|
+
|
|
422
|
+
let result = await sync(root)
|
|
423
|
+
expect(result.files[3]).toMatchObject({
|
|
424
|
+
name: "opencode-plugin",
|
|
425
|
+
path: join(root, ".opencode/plugins/agency-repository-skills.ts"),
|
|
426
|
+
state: "customized",
|
|
427
|
+
changed: false,
|
|
428
|
+
})
|
|
429
|
+
expect(
|
|
430
|
+
await Bun.file(
|
|
431
|
+
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
432
|
+
).exists(),
|
|
433
|
+
).toBe(false)
|
|
434
|
+
|
|
435
|
+
await unlink(join(root, ".opencode/plugins/agency-repository-skills.ts"))
|
|
436
|
+
await write(root, ".opencode/plugin/agency-repository-skills.ts", custom)
|
|
437
|
+
result = await sync(root)
|
|
438
|
+
expect(result.files[3]).toMatchObject({
|
|
439
|
+
path: join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
440
|
+
state: "customized",
|
|
441
|
+
changed: false,
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test("preserves user-owned TUI config and diagnostic plugin", async () => {
|
|
446
|
+
const customConfig = '{"theme":"custom"}\n'
|
|
447
|
+
const customPlugin =
|
|
448
|
+
"export default { id: 'agency.debug', tui: async () => {} }\n"
|
|
449
|
+
await write(root, ".opencode/tui.json", customConfig)
|
|
450
|
+
await write(root, ".opencode/tui/agency-debug.ts", customPlugin)
|
|
451
|
+
|
|
452
|
+
const result = await sync(root)
|
|
453
|
+
expect(result.files[4]).toMatchObject({
|
|
454
|
+
name: "opencode-tui",
|
|
455
|
+
path: join(root, ".opencode/tui.json"),
|
|
456
|
+
state: "customized",
|
|
457
|
+
changed: false,
|
|
458
|
+
remediation: expect.stringContaining("plugin list"),
|
|
459
|
+
})
|
|
460
|
+
expect(result.files[5]).toMatchObject({
|
|
461
|
+
name: "opencode-tui-plugin",
|
|
462
|
+
state: "customized",
|
|
463
|
+
changed: false,
|
|
464
|
+
})
|
|
465
|
+
expect(await Bun.file(join(root, ".opencode/tui.json")).text()).toBe(
|
|
466
|
+
customConfig,
|
|
467
|
+
)
|
|
468
|
+
expect(
|
|
469
|
+
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
|
|
470
|
+
).toBe(customPlugin)
|
|
471
|
+
})
|
|
472
|
+
|
|
275
473
|
test("preserves user-owned OpenCode commands at either supported path", async () => {
|
|
276
474
|
const custom = "---\ndescription: Custom Agency command\n---\n\nCustom.\n"
|
|
277
475
|
await write(root, ".opencode/commands/agency.md", custom)
|
|
@@ -14,11 +14,29 @@ import {
|
|
|
14
14
|
canUpdateManagedWorkbaseOpencodeCommand,
|
|
15
15
|
managedWorkbaseOpencodeCommand,
|
|
16
16
|
} from "../workbase/opencode-command-file"
|
|
17
|
+
import {
|
|
18
|
+
canUpdateManagedWorkbaseOpencodePlugin,
|
|
19
|
+
managedWorkbaseOpencodePlugin,
|
|
20
|
+
} from "../workbase/opencode-plugin-file"
|
|
21
|
+
import {
|
|
22
|
+
canUpdateManagedWorkbaseOpencodeTui,
|
|
23
|
+
managedWorkbaseOpencodeTui,
|
|
24
|
+
} from "../workbase/opencode-tui-file"
|
|
25
|
+
import {
|
|
26
|
+
canUpdateManagedWorkbaseOpencodeTuiPlugin,
|
|
27
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
28
|
+
} from "../workbase/opencode-tui-plugin-file"
|
|
17
29
|
|
|
18
30
|
type IntegrationFileState = "managed" | "customized" | "missing" | "drifted"
|
|
19
31
|
|
|
20
32
|
interface IntegrationFileStatus {
|
|
21
|
-
readonly name:
|
|
33
|
+
readonly name:
|
|
34
|
+
| "agents"
|
|
35
|
+
| "opencode"
|
|
36
|
+
| "opencode-command"
|
|
37
|
+
| "opencode-plugin"
|
|
38
|
+
| "opencode-tui"
|
|
39
|
+
| "opencode-tui-plugin"
|
|
22
40
|
readonly path: string
|
|
23
41
|
readonly state: IntegrationFileState
|
|
24
42
|
readonly diagnostic: string
|
|
@@ -75,6 +93,67 @@ const describe = (
|
|
|
75
93
|
"Run 'agency integration sync' to install the managed /agency command.",
|
|
76
94
|
}
|
|
77
95
|
}
|
|
96
|
+
if (name === "opencode-plugin") {
|
|
97
|
+
return state === "managed"
|
|
98
|
+
? {
|
|
99
|
+
diagnostic:
|
|
100
|
+
"Agency's managed OpenCode plugin exposes writable-checkout skills to work-item sessions.",
|
|
101
|
+
remediation: null,
|
|
102
|
+
}
|
|
103
|
+
: state === "customized"
|
|
104
|
+
? {
|
|
105
|
+
diagnostic:
|
|
106
|
+
"A user-owned OpenCode checkout-skill plugin is present and was preserved.",
|
|
107
|
+
remediation: null,
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
diagnostic:
|
|
111
|
+
"The managed OpenCode checkout-skill plugin needs synchronization.",
|
|
112
|
+
remediation:
|
|
113
|
+
"Run 'agency integration sync' to expose writable-checkout skills in OpenCode.",
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (name === "opencode-tui") {
|
|
117
|
+
return state === "managed"
|
|
118
|
+
? {
|
|
119
|
+
diagnostic:
|
|
120
|
+
"Agency's managed OpenCode TUI config explicitly loads /agency-debug.",
|
|
121
|
+
remediation: null,
|
|
122
|
+
}
|
|
123
|
+
: state === "customized"
|
|
124
|
+
? {
|
|
125
|
+
diagnostic:
|
|
126
|
+
"A user-owned OpenCode TUI config is present and was preserved.",
|
|
127
|
+
remediation:
|
|
128
|
+
"Add './tui/agency-debug.ts' to its plugin list to enable /agency-debug.",
|
|
129
|
+
}
|
|
130
|
+
: {
|
|
131
|
+
diagnostic:
|
|
132
|
+
"The managed OpenCode TUI config needs synchronization.",
|
|
133
|
+
remediation:
|
|
134
|
+
"Run 'agency integration sync' to register /agency-debug.",
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (name === "opencode-tui-plugin") {
|
|
138
|
+
return state === "managed"
|
|
139
|
+
? {
|
|
140
|
+
diagnostic:
|
|
141
|
+
"Agency's managed OpenCode TUI diagnostic companion is current.",
|
|
142
|
+
remediation: null,
|
|
143
|
+
}
|
|
144
|
+
: state === "customized"
|
|
145
|
+
? {
|
|
146
|
+
diagnostic:
|
|
147
|
+
"A user-owned OpenCode /agency-debug TUI plugin is present and was preserved.",
|
|
148
|
+
remediation: null,
|
|
149
|
+
}
|
|
150
|
+
: {
|
|
151
|
+
diagnostic:
|
|
152
|
+
"The managed OpenCode TUI diagnostic companion needs synchronization.",
|
|
153
|
+
remediation:
|
|
154
|
+
"Run 'agency integration sync' to install /agency-debug.",
|
|
155
|
+
}
|
|
156
|
+
}
|
|
78
157
|
|
|
79
158
|
return state === "missing" || state === "drifted"
|
|
80
159
|
? {
|
|
@@ -121,8 +200,21 @@ const inspect = (root: string) =>
|
|
|
121
200
|
const opencodeDirectory = join(root, ".opencode")
|
|
122
201
|
const opencodePath = join(opencodeDirectory, "opencode.jsonc")
|
|
123
202
|
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
203
|
+
const tuiPath = join(opencodeDirectory, "tui.jsonc")
|
|
204
|
+
const tuiJsonPath = join(opencodeDirectory, "tui.json")
|
|
124
205
|
const commandPath = join(opencodeDirectory, "command", "agency.md")
|
|
125
206
|
const pluralCommandPath = join(opencodeDirectory, "commands", "agency.md")
|
|
207
|
+
const pluginPath = join(
|
|
208
|
+
opencodeDirectory,
|
|
209
|
+
"plugin",
|
|
210
|
+
"agency-repository-skills.ts",
|
|
211
|
+
)
|
|
212
|
+
const pluralPluginPath = join(
|
|
213
|
+
opencodeDirectory,
|
|
214
|
+
"plugins",
|
|
215
|
+
"agency-repository-skills.ts",
|
|
216
|
+
)
|
|
217
|
+
const tuiPluginPath = join(opencodeDirectory, "tui", "agency-debug.ts")
|
|
126
218
|
const files: IntegrationFileStatus[] = []
|
|
127
219
|
|
|
128
220
|
files.push(
|
|
@@ -183,6 +275,64 @@ const inspect = (root: string) =>
|
|
|
183
275
|
files.push(fileStatus("opencode-command", commandPath, "missing"))
|
|
184
276
|
}
|
|
185
277
|
|
|
278
|
+
if ((yield* fs.readSymlinkTarget(pluginPath)) !== null) {
|
|
279
|
+
files.push(fileStatus("opencode-plugin", pluginPath, "customized"))
|
|
280
|
+
} else if (
|
|
281
|
+
(yield* fs.readSymlinkTarget(pluralPluginPath)) !== null ||
|
|
282
|
+
(yield* fs.exists(pluralPluginPath))
|
|
283
|
+
) {
|
|
284
|
+
files.push(fileStatus("opencode-plugin", pluralPluginPath, "customized"))
|
|
285
|
+
} else if (yield* fs.exists(pluginPath)) {
|
|
286
|
+
files.push(
|
|
287
|
+
classify(
|
|
288
|
+
"opencode-plugin",
|
|
289
|
+
pluginPath,
|
|
290
|
+
yield* fs.readFile(pluginPath),
|
|
291
|
+
managedWorkbaseOpencodePlugin,
|
|
292
|
+
canUpdateManagedWorkbaseOpencodePlugin,
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
} else {
|
|
296
|
+
files.push(fileStatus("opencode-plugin", pluginPath, "missing"))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if ((yield* fs.readSymlinkTarget(tuiPath)) !== null) {
|
|
300
|
+
files.push(fileStatus("opencode-tui", tuiPath, "customized"))
|
|
301
|
+
} else if (
|
|
302
|
+
(yield* fs.readSymlinkTarget(tuiJsonPath)) !== null ||
|
|
303
|
+
(yield* fs.exists(tuiJsonPath))
|
|
304
|
+
) {
|
|
305
|
+
files.push(fileStatus("opencode-tui", tuiJsonPath, "customized"))
|
|
306
|
+
} else if (yield* fs.exists(tuiPath)) {
|
|
307
|
+
files.push(
|
|
308
|
+
classify(
|
|
309
|
+
"opencode-tui",
|
|
310
|
+
tuiPath,
|
|
311
|
+
yield* fs.readFile(tuiPath),
|
|
312
|
+
managedWorkbaseOpencodeTui,
|
|
313
|
+
canUpdateManagedWorkbaseOpencodeTui,
|
|
314
|
+
),
|
|
315
|
+
)
|
|
316
|
+
} else {
|
|
317
|
+
files.push(fileStatus("opencode-tui", tuiPath, "missing"))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if ((yield* fs.readSymlinkTarget(tuiPluginPath)) !== null) {
|
|
321
|
+
files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "customized"))
|
|
322
|
+
} else if (yield* fs.exists(tuiPluginPath)) {
|
|
323
|
+
files.push(
|
|
324
|
+
classify(
|
|
325
|
+
"opencode-tui-plugin",
|
|
326
|
+
tuiPluginPath,
|
|
327
|
+
yield* fs.readFile(tuiPluginPath),
|
|
328
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
329
|
+
canUpdateManagedWorkbaseOpencodeTuiPlugin,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
} else {
|
|
333
|
+
files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "missing"))
|
|
334
|
+
}
|
|
335
|
+
|
|
186
336
|
return files
|
|
187
337
|
})
|
|
188
338
|
|
|
@@ -232,9 +382,21 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
232
382
|
} else if (status.name === "opencode") {
|
|
233
383
|
yield* fs.createDirectory(join(root, ".opencode"))
|
|
234
384
|
yield* fs.writeFile(status.path, managedWorkbaseOpencode)
|
|
235
|
-
} else {
|
|
385
|
+
} else if (status.name === "opencode-command") {
|
|
236
386
|
yield* fs.createDirectory(join(root, ".opencode", "command"))
|
|
237
387
|
yield* fs.writeFile(status.path, managedWorkbaseOpencodeCommand)
|
|
388
|
+
} else if (status.name === "opencode-plugin") {
|
|
389
|
+
yield* fs.createDirectory(join(root, ".opencode", "plugin"))
|
|
390
|
+
yield* fs.writeFile(status.path, managedWorkbaseOpencodePlugin)
|
|
391
|
+
} else if (status.name === "opencode-tui") {
|
|
392
|
+
yield* fs.createDirectory(join(root, ".opencode"))
|
|
393
|
+
yield* fs.writeFile(status.path, managedWorkbaseOpencodeTui)
|
|
394
|
+
} else {
|
|
395
|
+
yield* fs.createDirectory(join(root, ".opencode", "tui"))
|
|
396
|
+
yield* fs.writeFile(
|
|
397
|
+
status.path,
|
|
398
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
399
|
+
)
|
|
238
400
|
}
|
|
239
401
|
}
|
|
240
402
|
files.push({
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -72,7 +72,9 @@ a refinement loop, or pausing or handing off completed implementation work):
|
|
|
72
72
|
`missing` generated files. Agency keeps these instructions in
|
|
73
73
|
`.agency/AGENTS.md`, and its managed OpenCode config loads them automatically.
|
|
74
74
|
It also installs `.opencode/command/agency.md`, which provides safe `/agency`
|
|
75
|
-
workflows for active OpenCode sessions
|
|
75
|
+
workflows for active OpenCode sessions, a managed server plugin that exposes
|
|
76
|
+
skills from the authoritative writable checkout, and an explicitly registered
|
|
77
|
+
TUI companion providing `/agency-debug` without submitting an LLM prompt.
|
|
76
78
|
The workbase-root `AGENTS.md`, when present, belongs entirely to the workbase
|
|
77
79
|
owner and composes with these instructions through OpenCode's normal discovery.
|
|
78
80
|
`agency integration sync` updates only missing or checksum-safe drifted managed
|
|
@@ -81,6 +83,9 @@ files, and `agency work` reconciles them before launching an agent.
|
|
|
81
83
|
|
|
82
84
|
OpenCode can access the complete workbase tree, but this filesystem permission
|
|
83
85
|
does not expand Agency write authority beyond the checkout reported by
|
|
84
|
-
`agency context`. OpenCode
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
`agency context`. OpenCode remains rooted in the task or epic directory so the
|
|
87
|
+
workbase instructions and config compose normally. The managed plugin resolves
|
|
88
|
+
the writable checkout from launch context or `agency context`, then adds its
|
|
89
|
+
supported skill directories through `skills.paths`; this does not make other
|
|
90
|
+
checkout-local OpenCode configuration authoritative. Agents must follow the
|
|
91
|
+
authority reported by `agency context`.
|
|
@@ -0,0 +1,61 @@
|
|
|
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 } from "node:fs"
|
|
10
|
+
import { join, sep } from "node:path"
|
|
11
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
12
|
+
|
|
13
|
+
const contextCheckout = async (directory: string) => {
|
|
14
|
+
const task = process.env.AGENCY_TASK_ID
|
|
15
|
+
const phase = process.env.AGENCY_PHASE_ID
|
|
16
|
+
const args = task
|
|
17
|
+
? ["agency", "context", "--task", task, ...(phase ? ["--phase", phase] : []), "--compact", "--json"]
|
|
18
|
+
: ["agency", "context", ".", "--compact", "--json"]
|
|
19
|
+
const child = Bun.spawn(args, { cwd: directory, stdout: "pipe", stderr: "ignore" })
|
|
20
|
+
const output = await new Response(child.stdout).text()
|
|
21
|
+
if ((await child.exited) !== 0) return
|
|
22
|
+
const envelope = JSON.parse(output)
|
|
23
|
+
if (envelope.ok !== true) return
|
|
24
|
+
return envelope.result?.authority?.writable?.checkoutPath as string | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const plugin: Plugin = async ({ directory }) => ({
|
|
28
|
+
config: async (config) => {
|
|
29
|
+
const checkout =
|
|
30
|
+
process.env.AGENCY_WRITABLE_CHECKOUT ??
|
|
31
|
+
(await contextCheckout(directory).catch(() => undefined))
|
|
32
|
+
if (!checkout) return
|
|
33
|
+
|
|
34
|
+
const paths = [
|
|
35
|
+
join(checkout, ".claude", "skills"),
|
|
36
|
+
join(checkout, ".agents", "skills"),
|
|
37
|
+
join(checkout, ".opencode", "skill"),
|
|
38
|
+
join(checkout, ".opencode", "skills"),
|
|
39
|
+
].filter(existsSync).map((path) => \`\${path}\${sep}.\`)
|
|
40
|
+
if (paths.length === 0) return
|
|
41
|
+
|
|
42
|
+
config.skills ??= {}
|
|
43
|
+
config.skills.paths = [...new Set([...(config.skills.paths ?? []), ...paths])]
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export default plugin
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
const renderManagedWorkbaseOpencodePlugin = (content: string) =>
|
|
51
|
+
`// agency-managed: sha256=${checksum(content)}\n\n${content}`
|
|
52
|
+
|
|
53
|
+
export const managedWorkbaseOpencodePlugin =
|
|
54
|
+
renderManagedWorkbaseOpencodePlugin(body)
|
|
55
|
+
|
|
56
|
+
export const canUpdateManagedWorkbaseOpencodePlugin = (content: string) => {
|
|
57
|
+
const match = content.match(managedHeaderPattern)
|
|
58
|
+
if (!match?.[1]) return false
|
|
59
|
+
|
|
60
|
+
return checksum(content.slice(match[0].length)) === match[1]
|
|
61
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 = () =>
|
|
10
|
+
`${JSON.stringify(
|
|
11
|
+
{
|
|
12
|
+
$schema: "https://opencode.ai/tui.json",
|
|
13
|
+
plugin: ["./tui/agency-debug.ts"],
|
|
14
|
+
},
|
|
15
|
+
null,
|
|
16
|
+
2,
|
|
17
|
+
)}\n`
|
|
18
|
+
|
|
19
|
+
const renderManagedWorkbaseOpencodeTui = (content: string) =>
|
|
20
|
+
`// agency-managed: sha256=${checksum(content)}\n\n${content}`
|
|
21
|
+
|
|
22
|
+
export const managedWorkbaseOpencodeTui =
|
|
23
|
+
renderManagedWorkbaseOpencodeTui(body())
|
|
24
|
+
|
|
25
|
+
export const canUpdateManagedWorkbaseOpencodeTui = (content: string) => {
|
|
26
|
+
const match = content.match(managedHeaderPattern)
|
|
27
|
+
if (!match?.[1]) return false
|
|
28
|
+
|
|
29
|
+
return checksum(content.slice(match[0].length)) === match[1]
|
|
30
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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 type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
10
|
+
|
|
11
|
+
const serverMarker = /[\\\\/]\\.$/
|
|
12
|
+
|
|
13
|
+
const tui: TuiPlugin = async (api) => {
|
|
14
|
+
api.keymap.registerLayer({
|
|
15
|
+
commands: [
|
|
16
|
+
{
|
|
17
|
+
name: "agency.debug",
|
|
18
|
+
title: "Agency integration diagnostic",
|
|
19
|
+
desc: "Check Agency's OpenCode TUI and server plugin initialization",
|
|
20
|
+
category: "Agency",
|
|
21
|
+
namespace: "palette",
|
|
22
|
+
slashName: "agency-debug",
|
|
23
|
+
run() {
|
|
24
|
+
const paths = api.state.config.skills?.paths
|
|
25
|
+
const serverInitialized = paths?.some(
|
|
26
|
+
(path) => typeof path === "string" && serverMarker.test(path),
|
|
27
|
+
)
|
|
28
|
+
const message = serverInitialized
|
|
29
|
+
? "TUI companion: initialized. Server plugin: initialized; checkout skills registered."
|
|
30
|
+
: api.state.ready
|
|
31
|
+
? "TUI companion: initialized. Server plugin: indeterminate; no checkout skill marker is present."
|
|
32
|
+
: "TUI companion: initialized. Server plugin: indeterminate; server state is not ready."
|
|
33
|
+
|
|
34
|
+
api.ui.toast({
|
|
35
|
+
variant: serverInitialized ? "success" : "warning",
|
|
36
|
+
title: "Agency integration",
|
|
37
|
+
message,
|
|
38
|
+
duration: 5000,
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
47
|
+
id: "agency.debug",
|
|
48
|
+
tui,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default plugin
|
|
52
|
+
`
|
|
53
|
+
|
|
54
|
+
const renderManagedWorkbaseOpencodeTuiPlugin = (content: string) =>
|
|
55
|
+
`// agency-managed: sha256=${checksum(content)}\n\n${content}`
|
|
56
|
+
|
|
57
|
+
export const managedWorkbaseOpencodeTuiPlugin =
|
|
58
|
+
renderManagedWorkbaseOpencodeTuiPlugin(body)
|
|
59
|
+
|
|
60
|
+
export const canUpdateManagedWorkbaseOpencodeTuiPlugin = (content: string) => {
|
|
61
|
+
const match = content.match(managedHeaderPattern)
|
|
62
|
+
if (!match?.[1]) return false
|
|
63
|
+
|
|
64
|
+
return checksum(content.slice(match[0].length)) === match[1]
|
|
65
|
+
}
|