@markjaquith/agency 2.45.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 +11 -3
- package/package.json +1 -1
- package/src/cli.test.ts +14 -2
- package/src/commands/init.test.ts +11 -0
- package/src/commands/integration.test.ts +38 -2
- package/src/commands/integration.ts +6 -2
- package/src/services/IntegrationService.test.ts +124 -0
- package/src/services/IntegrationService.ts +106 -2
- package/src/workbase/AGENTS.md +3 -2
- package/src/workbase/opencode-plugin-file.ts +2 -2
- package/src/workbase/opencode-tui-file.ts +30 -0
- package/src/workbase/opencode-tui-plugin-file.ts +65 -0
package/README.md
CHANGED
|
@@ -52,8 +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
|
|
55
56
|
command/agency.md # managed /agency workflow command
|
|
56
57
|
plugin/agency-repository-skills.ts # managed checkout skill discovery
|
|
58
|
+
tui/agency-debug.ts # managed /agency-debug TUI diagnostic
|
|
57
59
|
agency.json # tracked config and portable repository declarations
|
|
58
60
|
repos/ # ignored local materializations
|
|
59
61
|
frontend/ # bare Git repository or symlink
|
|
@@ -82,9 +84,10 @@ workbase/
|
|
|
82
84
|
|
|
83
85
|
Agency keeps discovery and other observational commands read-only. Run
|
|
84
86
|
`agency integration status` to inspect `.agency/AGENTS.md` and
|
|
85
|
-
`.opencode/opencode.jsonc`,
|
|
86
|
-
integration sync` to create missing files or refresh
|
|
87
|
-
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
|
|
88
91
|
`AGENTS.md` is user-owned and is not inspected or modified by Agency.
|
|
89
92
|
|
|
90
93
|
When upgrading an existing workbase, synchronization moves a checksum-valid
|
|
@@ -99,6 +102,11 @@ and replaces the built-in Plan agent with `agency-plan`. That planning agent can
|
|
|
99
102
|
update `TASK.md`, `PHASE.md`, and `EPIC.md` while other edits remain disabled.
|
|
100
103
|
When the subagent launches work in another agent, it verifies that the runner
|
|
101
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.
|
|
102
110
|
OpenCode discovers the config from task and epic launch directories. Agents
|
|
103
111
|
receive whole-workbase visibility from that reference. Bash and Agency operations
|
|
104
112
|
must still follow the write authority reported by `agency context`.
|
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
2
|
import { access, mkdir, realpath, symlink } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
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"
|
|
@@ -528,6 +528,8 @@ describe("CLI", () => {
|
|
|
528
528
|
{ name: "opencode", state: "managed" },
|
|
529
529
|
{ name: "opencode-command", state: "managed" },
|
|
530
530
|
{ name: "opencode-plugin", state: "managed" },
|
|
531
|
+
{ name: "opencode-tui", state: "managed" },
|
|
532
|
+
{ name: "opencode-tui-plugin", state: "managed" },
|
|
531
533
|
])
|
|
532
534
|
|
|
533
535
|
const synced = parseJson(
|
|
@@ -538,6 +540,8 @@ describe("CLI", () => {
|
|
|
538
540
|
{ name: "opencode", state: "managed", changed: false },
|
|
539
541
|
{ name: "opencode-command", state: "managed", changed: false },
|
|
540
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 },
|
|
541
545
|
])
|
|
542
546
|
})
|
|
543
547
|
|
|
@@ -1097,7 +1101,9 @@ status: open
|
|
|
1097
1101
|
]),
|
|
1098
1102
|
)
|
|
1099
1103
|
if (launch.skillPath) {
|
|
1100
|
-
expect(effectiveConfig.skills.paths).toContain(
|
|
1104
|
+
expect(effectiveConfig.skills.paths).toContain(
|
|
1105
|
+
`${launch.skillPath}${sep}.`,
|
|
1106
|
+
)
|
|
1101
1107
|
const skillProbe = Bun.spawnSync(
|
|
1102
1108
|
[
|
|
1103
1109
|
"opencode",
|
|
@@ -1115,6 +1121,12 @@ status: open
|
|
|
1115
1121
|
expect(
|
|
1116
1122
|
JSON.parse(skillProbe.stdout.toString()).result.output,
|
|
1117
1123
|
).toContain("Repository skill content.")
|
|
1124
|
+
} else {
|
|
1125
|
+
expect(
|
|
1126
|
+
(effectiveConfig.skills?.paths ?? []).filter((path: string) =>
|
|
1127
|
+
path.endsWith(`${sep}.`),
|
|
1128
|
+
),
|
|
1129
|
+
).toEqual([])
|
|
1118
1130
|
}
|
|
1119
1131
|
if (launch === launches[0]) {
|
|
1120
1132
|
expect(effectiveConfig.instructions).toContain(".agency/AGENTS.md")
|
|
@@ -71,6 +71,17 @@ describe("init command", () => {
|
|
|
71
71
|
).text()
|
|
72
72
|
expect(plugin).toContain("AGENCY_WRITABLE_CHECKOUT")
|
|
73
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")
|
|
74
85
|
})
|
|
75
86
|
|
|
76
87
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -46,6 +46,16 @@ describe("integration command", () => {
|
|
|
46
46
|
state: "missing",
|
|
47
47
|
remediation: expect.stringContaining("integration sync"),
|
|
48
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
|
+
},
|
|
49
59
|
],
|
|
50
60
|
})
|
|
51
61
|
})
|
|
@@ -89,7 +99,17 @@ OpenCode /agency command: missing
|
|
|
89
99
|
OpenCode checkout skills: missing
|
|
90
100
|
Path: .opencode/plugin/agency-repository-skills.ts
|
|
91
101
|
The managed OpenCode checkout-skill plugin needs synchronization.
|
|
92
|
-
Action: Run 'agency integration sync' to expose writable-checkout skills in OpenCode
|
|
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.`)
|
|
93
113
|
})
|
|
94
114
|
|
|
95
115
|
test("explicitly synchronizes integration files", async () => {
|
|
@@ -102,6 +122,8 @@ OpenCode checkout skills: missing
|
|
|
102
122
|
{ name: "opencode", state: "managed", changed: true },
|
|
103
123
|
{ name: "opencode-command", state: "managed", changed: true },
|
|
104
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 },
|
|
105
127
|
])
|
|
106
128
|
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
107
129
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(true)
|
|
@@ -116,6 +138,12 @@ OpenCode checkout skills: missing
|
|
|
116
138
|
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
117
139
|
).exists(),
|
|
118
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)
|
|
119
147
|
})
|
|
120
148
|
|
|
121
149
|
test("formats integration sync results for people", async () => {
|
|
@@ -139,6 +167,14 @@ OpenCode /agency command: synced
|
|
|
139
167
|
|
|
140
168
|
OpenCode checkout skills: synced
|
|
141
169
|
Path: .opencode/plugin/agency-repository-skills.ts
|
|
142
|
-
Agency's managed OpenCode plugin exposes writable-checkout skills to work-item sessions
|
|
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.`)
|
|
143
179
|
})
|
|
144
180
|
})
|
|
@@ -17,6 +17,8 @@ interface IntegrationResult {
|
|
|
17
17
|
| "opencode"
|
|
18
18
|
| "opencode-command"
|
|
19
19
|
| "opencode-plugin"
|
|
20
|
+
| "opencode-tui"
|
|
21
|
+
| "opencode-tui-plugin"
|
|
20
22
|
readonly path: string
|
|
21
23
|
readonly state: string
|
|
22
24
|
readonly diagnostic: string
|
|
@@ -30,6 +32,8 @@ const integrationNames = {
|
|
|
30
32
|
opencode: "OpenCode config",
|
|
31
33
|
"opencode-command": "OpenCode /agency command",
|
|
32
34
|
"opencode-plugin": "OpenCode checkout skills",
|
|
35
|
+
"opencode-tui": "OpenCode TUI config",
|
|
36
|
+
"opencode-tui-plugin": "OpenCode /agency-debug",
|
|
33
37
|
} as const
|
|
34
38
|
|
|
35
39
|
const logHumanResult = (
|
|
@@ -88,8 +92,8 @@ Usage: agency integration <subcommand>
|
|
|
88
92
|
|
|
89
93
|
Inspect or explicitly synchronize managed agent integration files. OpenCode
|
|
90
94
|
launches load the managed instructions and project config at runtime, and expose
|
|
91
|
-
the managed /agency command
|
|
92
|
-
Agency write authority.
|
|
95
|
+
the managed /agency command, writable-checkout skills, and /agency-debug TUI
|
|
96
|
+
diagnostic, without changing Agency write authority.
|
|
93
97
|
|
|
94
98
|
Subcommands:
|
|
95
99
|
status Report file state, access diagnostics, and safe remediation
|
|
@@ -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 {
|
|
@@ -14,6 +15,11 @@ import {
|
|
|
14
15
|
canUpdateManagedWorkbaseOpencodePlugin,
|
|
15
16
|
managedWorkbaseOpencodePlugin,
|
|
16
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"
|
|
17
23
|
import { IntegrationService } from "./IntegrationService"
|
|
18
24
|
|
|
19
25
|
const write = async (root: string, path: string, content: string) => {
|
|
@@ -56,6 +62,8 @@ describe("IntegrationService", () => {
|
|
|
56
62
|
"missing",
|
|
57
63
|
"missing",
|
|
58
64
|
"missing",
|
|
65
|
+
"missing",
|
|
66
|
+
"missing",
|
|
59
67
|
])
|
|
60
68
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(false)
|
|
61
69
|
|
|
@@ -71,11 +79,19 @@ describe("IntegrationService", () => {
|
|
|
71
79
|
".opencode/plugin/agency-repository-skills.ts",
|
|
72
80
|
managedWorkbaseOpencodePlugin,
|
|
73
81
|
)
|
|
82
|
+
await write(root, ".opencode/tui.jsonc", managedWorkbaseOpencodeTui)
|
|
83
|
+
await write(
|
|
84
|
+
root,
|
|
85
|
+
".opencode/tui/agency-debug.ts",
|
|
86
|
+
managedWorkbaseOpencodeTuiPlugin,
|
|
87
|
+
)
|
|
74
88
|
expect((await status(root)).files.map(({ state }) => state)).toEqual([
|
|
75
89
|
"managed",
|
|
76
90
|
"managed",
|
|
77
91
|
"managed",
|
|
78
92
|
"managed",
|
|
93
|
+
"managed",
|
|
94
|
+
"managed",
|
|
79
95
|
])
|
|
80
96
|
})
|
|
81
97
|
|
|
@@ -92,6 +108,8 @@ describe("IntegrationService", () => {
|
|
|
92
108
|
"drifted",
|
|
93
109
|
"missing",
|
|
94
110
|
"missing",
|
|
111
|
+
"missing",
|
|
112
|
+
"missing",
|
|
95
113
|
])
|
|
96
114
|
})
|
|
97
115
|
|
|
@@ -114,6 +132,9 @@ describe("IntegrationService", () => {
|
|
|
114
132
|
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
115
133
|
"config.skills.paths = [...new Set",
|
|
116
134
|
)
|
|
135
|
+
expect(managedWorkbaseOpencodePlugin).toContain(
|
|
136
|
+
".map((path) => `${path}${sep}.`)",
|
|
137
|
+
)
|
|
117
138
|
expect(
|
|
118
139
|
canUpdateManagedWorkbaseOpencodePlugin(managedWorkbaseOpencodePlugin),
|
|
119
140
|
).toBe(true)
|
|
@@ -124,6 +145,73 @@ describe("IntegrationService", () => {
|
|
|
124
145
|
).toBe(false)
|
|
125
146
|
})
|
|
126
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
|
+
|
|
127
215
|
test("generates a positional OpenCode command for Agency workflows", () => {
|
|
128
216
|
expect(managedWorkbaseOpencodeCommand).toContain(
|
|
129
217
|
"description: Operate Agency work",
|
|
@@ -289,6 +377,8 @@ describe("IntegrationService", () => {
|
|
|
289
377
|
{ name: "opencode", state: "managed", changed: true },
|
|
290
378
|
{ name: "opencode-command", state: "managed", changed: true },
|
|
291
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 },
|
|
292
382
|
])
|
|
293
383
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
294
384
|
customRootAgents,
|
|
@@ -307,6 +397,12 @@ describe("IntegrationService", () => {
|
|
|
307
397
|
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
308
398
|
).text(),
|
|
309
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)
|
|
310
406
|
|
|
311
407
|
await unlink(join(root, ".agency/AGENTS.md"))
|
|
312
408
|
const second = await sync(root)
|
|
@@ -346,6 +442,34 @@ describe("IntegrationService", () => {
|
|
|
346
442
|
})
|
|
347
443
|
})
|
|
348
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
|
+
|
|
349
473
|
test("preserves user-owned OpenCode commands at either supported path", async () => {
|
|
350
474
|
const custom = "---\ndescription: Custom Agency command\n---\n\nCustom.\n"
|
|
351
475
|
await write(root, ".opencode/commands/agency.md", custom)
|
|
@@ -18,11 +18,25 @@ import {
|
|
|
18
18
|
canUpdateManagedWorkbaseOpencodePlugin,
|
|
19
19
|
managedWorkbaseOpencodePlugin,
|
|
20
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"
|
|
21
29
|
|
|
22
30
|
type IntegrationFileState = "managed" | "customized" | "missing" | "drifted"
|
|
23
31
|
|
|
24
32
|
interface IntegrationFileStatus {
|
|
25
|
-
readonly name:
|
|
33
|
+
readonly name:
|
|
34
|
+
| "agents"
|
|
35
|
+
| "opencode"
|
|
36
|
+
| "opencode-command"
|
|
37
|
+
| "opencode-plugin"
|
|
38
|
+
| "opencode-tui"
|
|
39
|
+
| "opencode-tui-plugin"
|
|
26
40
|
readonly path: string
|
|
27
41
|
readonly state: IntegrationFileState
|
|
28
42
|
readonly diagnostic: string
|
|
@@ -99,6 +113,47 @@ const describe = (
|
|
|
99
113
|
"Run 'agency integration sync' to expose writable-checkout skills in OpenCode.",
|
|
100
114
|
}
|
|
101
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
|
+
}
|
|
102
157
|
|
|
103
158
|
return state === "missing" || state === "drifted"
|
|
104
159
|
? {
|
|
@@ -145,6 +200,8 @@ const inspect = (root: string) =>
|
|
|
145
200
|
const opencodeDirectory = join(root, ".opencode")
|
|
146
201
|
const opencodePath = join(opencodeDirectory, "opencode.jsonc")
|
|
147
202
|
const opencodeJsonPath = join(opencodeDirectory, "opencode.json")
|
|
203
|
+
const tuiPath = join(opencodeDirectory, "tui.jsonc")
|
|
204
|
+
const tuiJsonPath = join(opencodeDirectory, "tui.json")
|
|
148
205
|
const commandPath = join(opencodeDirectory, "command", "agency.md")
|
|
149
206
|
const pluralCommandPath = join(opencodeDirectory, "commands", "agency.md")
|
|
150
207
|
const pluginPath = join(
|
|
@@ -157,6 +214,7 @@ const inspect = (root: string) =>
|
|
|
157
214
|
"plugins",
|
|
158
215
|
"agency-repository-skills.ts",
|
|
159
216
|
)
|
|
217
|
+
const tuiPluginPath = join(opencodeDirectory, "tui", "agency-debug.ts")
|
|
160
218
|
const files: IntegrationFileStatus[] = []
|
|
161
219
|
|
|
162
220
|
files.push(
|
|
@@ -238,6 +296,43 @@ const inspect = (root: string) =>
|
|
|
238
296
|
files.push(fileStatus("opencode-plugin", pluginPath, "missing"))
|
|
239
297
|
}
|
|
240
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
|
+
|
|
241
336
|
return files
|
|
242
337
|
})
|
|
243
338
|
|
|
@@ -290,9 +385,18 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
|
|
|
290
385
|
} else if (status.name === "opencode-command") {
|
|
291
386
|
yield* fs.createDirectory(join(root, ".opencode", "command"))
|
|
292
387
|
yield* fs.writeFile(status.path, managedWorkbaseOpencodeCommand)
|
|
293
|
-
} else {
|
|
388
|
+
} else if (status.name === "opencode-plugin") {
|
|
294
389
|
yield* fs.createDirectory(join(root, ".opencode", "plugin"))
|
|
295
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
|
+
)
|
|
296
400
|
}
|
|
297
401
|
}
|
|
298
402
|
files.push({
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -72,8 +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,
|
|
76
|
-
from the authoritative writable checkout
|
|
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.
|
|
77
78
|
The workbase-root `AGENTS.md`, when present, belongs entirely to the workbase
|
|
78
79
|
owner and composes with these instructions through OpenCode's normal discovery.
|
|
79
80
|
`agency integration sync` updates only missing or checksum-safe drifted managed
|
|
@@ -7,7 +7,7 @@ const checksum = (content: string) =>
|
|
|
7
7
|
createHash("sha256").update(content).digest("hex")
|
|
8
8
|
|
|
9
9
|
const body = `import { existsSync } from "node:fs"
|
|
10
|
-
import { join } from "node:path"
|
|
10
|
+
import { join, sep } from "node:path"
|
|
11
11
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
12
12
|
|
|
13
13
|
const contextCheckout = async (directory: string) => {
|
|
@@ -36,7 +36,7 @@ const plugin: Plugin = async ({ directory }) => ({
|
|
|
36
36
|
join(checkout, ".agents", "skills"),
|
|
37
37
|
join(checkout, ".opencode", "skill"),
|
|
38
38
|
join(checkout, ".opencode", "skills"),
|
|
39
|
-
].filter(existsSync)
|
|
39
|
+
].filter(existsSync).map((path) => \`\${path}\${sep}.\`)
|
|
40
40
|
if (paths.length === 0) return
|
|
41
41
|
|
|
42
42
|
config.skills ??= {}
|
|
@@ -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
|
+
}
|