@markjaquith/agency 2.45.0 → 2.47.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 +16 -4
- package/package.json +1 -1
- package/src/cli.test.ts +14 -2
- package/src/commands/init.test.ts +14 -0
- package/src/commands/integration.test.ts +38 -2
- package/src/commands/integration.ts +6 -2
- package/src/services/IntegrationService.test.ts +145 -1
- package/src/services/IntegrationService.ts +106 -2
- package/src/workbase/AGENTS.md +3 -2
- package/src/workbase/opencode-file.ts +13 -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
|
|
@@ -96,9 +99,18 @@ The OpenCode config defines an `@agency` subagent for delegated workbase
|
|
|
96
99
|
orchestration, loads Agency's hidden instructions in addition to any user-owned
|
|
97
100
|
root `AGENTS.md`, advertises the complete workbase as one portable reference,
|
|
98
101
|
and replaces the built-in Plan agent with `agency-plan`. That planning agent can
|
|
99
|
-
update `TASK.md`, `PHASE.md`, and `EPIC.md
|
|
102
|
+
update `TASK.md`, `PHASE.md`, and `EPIC.md`, inspect the workbase through
|
|
103
|
+
read-only Agency commands, and use explicit Agency CLI permissions to create or
|
|
104
|
+
update planning structure. Its normal research tools and the complete Agency CLI
|
|
105
|
+
remain available; managed Agency instructions and reported authority govern each
|
|
106
|
+
operation.
|
|
100
107
|
When the subagent launches work in another agent, it verifies that the runner
|
|
101
108
|
started and returns without waiting for the task to finish.
|
|
109
|
+
The TUI-only `/agency-debug` command reports TUI companion initialization and
|
|
110
|
+
whether the server plugin registered writable-checkout skills. It uses a native
|
|
111
|
+
toast and does not submit a prompt to an LLM. When no writable checkout skill
|
|
112
|
+
directory is available, server initialization is reported as indeterminate
|
|
113
|
+
rather than inferred from plugin discovery.
|
|
102
114
|
OpenCode discovers the config from task and epic launch directories. Agents
|
|
103
115
|
receive whole-workbase visibility from that reference. Bash and Agency operations
|
|
104
116
|
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")
|
|
@@ -49,6 +49,9 @@ describe("init command", () => {
|
|
|
49
49
|
expect(config.agent["agency-plan"]).toMatchObject({
|
|
50
50
|
mode: "primary",
|
|
51
51
|
permission: {
|
|
52
|
+
bash: {
|
|
53
|
+
"agency *": "allow",
|
|
54
|
+
},
|
|
52
55
|
edit: {
|
|
53
56
|
"*": "deny",
|
|
54
57
|
"tasks/*/TASK.md": "allow",
|
|
@@ -71,6 +74,17 @@ describe("init command", () => {
|
|
|
71
74
|
).text()
|
|
72
75
|
expect(plugin).toContain("AGENCY_WRITABLE_CHECKOUT")
|
|
73
76
|
expect(plugin).toContain("config.skills.paths")
|
|
77
|
+
const tui = await Bun.file(join(root, ".opencode/tui.jsonc")).text()
|
|
78
|
+
expect(JSON.parse(tui.slice(tui.indexOf("\n\n") + 2))).toEqual({
|
|
79
|
+
$schema: "https://opencode.ai/tui.json",
|
|
80
|
+
plugin: ["./tui/agency-debug.ts"],
|
|
81
|
+
})
|
|
82
|
+
const tuiPlugin = await Bun.file(
|
|
83
|
+
join(root, ".opencode/tui/agency-debug.ts"),
|
|
84
|
+
).text()
|
|
85
|
+
expect(tuiPlugin).toContain('slashName: "agency-debug"')
|
|
86
|
+
expect(tuiPlugin).toContain("api.ui.toast")
|
|
87
|
+
expect(tuiPlugin).not.toContain("chat.message")
|
|
74
88
|
})
|
|
75
89
|
|
|
76
90
|
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",
|
|
@@ -195,11 +283,14 @@ describe("IntegrationService", () => {
|
|
|
195
283
|
},
|
|
196
284
|
"agency-plan": {
|
|
197
285
|
description:
|
|
198
|
-
"Agency planning mode. May
|
|
286
|
+
"Agency planning mode. May update Agency plans and planning structure.",
|
|
199
287
|
mode: "primary",
|
|
200
288
|
prompt: expect.stringContaining("You are in Agency Plan mode"),
|
|
201
289
|
permission: {
|
|
202
290
|
question: "allow",
|
|
291
|
+
bash: {
|
|
292
|
+
"agency *": "allow",
|
|
293
|
+
},
|
|
203
294
|
edit: {
|
|
204
295
|
"*": "deny",
|
|
205
296
|
"tasks/*/TASK.md": "allow",
|
|
@@ -219,6 +310,23 @@ describe("IntegrationService", () => {
|
|
|
219
310
|
expect(config.agent.agency.prompt).toContain(
|
|
220
311
|
"return without waiting for the task to finish",
|
|
221
312
|
)
|
|
313
|
+
expect(config.agent["agency-plan"].prompt).toContain(
|
|
314
|
+
"Start with `agency context . --json`",
|
|
315
|
+
)
|
|
316
|
+
expect(config.agent["agency-plan"].prompt).toContain(
|
|
317
|
+
"decompose it into independently deliverable tasks",
|
|
318
|
+
)
|
|
319
|
+
expect(config.agent["agency-plan"].prompt).toContain("Use `--if-revision`")
|
|
320
|
+
expect(config.agent["agency-plan"].prompt).toContain(
|
|
321
|
+
"available ticket tools",
|
|
322
|
+
)
|
|
323
|
+
expect(config.agent["agency-plan"].prompt).toContain(
|
|
324
|
+
"changing lifecycle state",
|
|
325
|
+
)
|
|
326
|
+
expect(config.agent["agency-plan"].prompt).toContain(
|
|
327
|
+
"Follow the managed Agency instructions and reported authority",
|
|
328
|
+
)
|
|
329
|
+
expect(config.agent["agency-plan"].permission.bash["*"]).toBeUndefined()
|
|
222
330
|
expect(config.references).toEqual({
|
|
223
331
|
workbase: {
|
|
224
332
|
path: "..",
|
|
@@ -289,6 +397,8 @@ describe("IntegrationService", () => {
|
|
|
289
397
|
{ name: "opencode", state: "managed", changed: true },
|
|
290
398
|
{ name: "opencode-command", state: "managed", changed: true },
|
|
291
399
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
400
|
+
{ name: "opencode-tui", state: "managed", changed: true },
|
|
401
|
+
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
292
402
|
])
|
|
293
403
|
expect(await Bun.file(join(root, "AGENTS.md")).text()).toBe(
|
|
294
404
|
customRootAgents,
|
|
@@ -307,6 +417,12 @@ describe("IntegrationService", () => {
|
|
|
307
417
|
join(root, ".opencode/plugin/agency-repository-skills.ts"),
|
|
308
418
|
).text(),
|
|
309
419
|
).toBe(managedWorkbaseOpencodePlugin)
|
|
420
|
+
expect(await Bun.file(join(root, ".opencode/tui.jsonc")).text()).toBe(
|
|
421
|
+
managedWorkbaseOpencodeTui,
|
|
422
|
+
)
|
|
423
|
+
expect(
|
|
424
|
+
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
|
|
425
|
+
).toBe(managedWorkbaseOpencodeTuiPlugin)
|
|
310
426
|
|
|
311
427
|
await unlink(join(root, ".agency/AGENTS.md"))
|
|
312
428
|
const second = await sync(root)
|
|
@@ -346,6 +462,34 @@ describe("IntegrationService", () => {
|
|
|
346
462
|
})
|
|
347
463
|
})
|
|
348
464
|
|
|
465
|
+
test("preserves user-owned TUI config and diagnostic plugin", async () => {
|
|
466
|
+
const customConfig = '{"theme":"custom"}\n'
|
|
467
|
+
const customPlugin =
|
|
468
|
+
"export default { id: 'agency.debug', tui: async () => {} }\n"
|
|
469
|
+
await write(root, ".opencode/tui.json", customConfig)
|
|
470
|
+
await write(root, ".opencode/tui/agency-debug.ts", customPlugin)
|
|
471
|
+
|
|
472
|
+
const result = await sync(root)
|
|
473
|
+
expect(result.files[4]).toMatchObject({
|
|
474
|
+
name: "opencode-tui",
|
|
475
|
+
path: join(root, ".opencode/tui.json"),
|
|
476
|
+
state: "customized",
|
|
477
|
+
changed: false,
|
|
478
|
+
remediation: expect.stringContaining("plugin list"),
|
|
479
|
+
})
|
|
480
|
+
expect(result.files[5]).toMatchObject({
|
|
481
|
+
name: "opencode-tui-plugin",
|
|
482
|
+
state: "customized",
|
|
483
|
+
changed: false,
|
|
484
|
+
})
|
|
485
|
+
expect(await Bun.file(join(root, ".opencode/tui.json")).text()).toBe(
|
|
486
|
+
customConfig,
|
|
487
|
+
)
|
|
488
|
+
expect(
|
|
489
|
+
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).text(),
|
|
490
|
+
).toBe(customPlugin)
|
|
491
|
+
})
|
|
492
|
+
|
|
349
493
|
test("preserves user-owned OpenCode commands at either supported path", async () => {
|
|
350
494
|
const custom = "---\ndescription: Custom Agency command\n---\n\nCustom.\n"
|
|
351
495
|
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
|
|
@@ -8,7 +8,17 @@ const checksum = (content: string) =>
|
|
|
8
8
|
|
|
9
9
|
const agencyPlanPrompt = `You are in Agency Plan mode. Think, read, search, and delegate exploration to construct a well-formed plan for the user's goal. Keep the plan comprehensive but concise, and ask clarifying questions when important tradeoffs or intent are unclear.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Start with \`agency context . --json\`. Use its document paths and revisions, then inspect the graph, related epics, tasks, phases, linked tickets, and repository declarations needed to understand the work. Use machine-readable Agency output when available instead of inferring structure from directory names.
|
|
12
|
+
|
|
13
|
+
When planning an epic, decompose it into independently deliverable tasks with explicit dependencies. Add phases only when one task genuinely requires multiple ordered delivery units. Reuse or update existing work instead of creating duplicate tasks or phases.
|
|
14
|
+
|
|
15
|
+
Use the Agency CLI for full workbase orchestration when the plan requires it, including creating or updating related epics, tasks, and phases; moving tasks; maintaining dependencies; and changing lifecycle state. Use \`--if-revision\` with the revision returned by context for mutations that support it, and run \`agency validate\` after changing workbase structure. Use available ticket tools to inspect or update a linked external ticket when the plan requires it.
|
|
16
|
+
|
|
17
|
+
You may edit TASK.md, PHASE.md, and EPIC.md to record the outcome, current approach, and important decisions. Do not edit any other file or use shell commands to modify the system. Follow the managed Agency instructions and reported authority for every Agency operation.`
|
|
18
|
+
|
|
19
|
+
const agencyPlanBashPermissions = {
|
|
20
|
+
"agency *": "allow",
|
|
21
|
+
}
|
|
12
22
|
|
|
13
23
|
const body = () =>
|
|
14
24
|
`${JSON.stringify(
|
|
@@ -28,11 +38,12 @@ const body = () =>
|
|
|
28
38
|
},
|
|
29
39
|
"agency-plan": {
|
|
30
40
|
description:
|
|
31
|
-
"Agency planning mode. May
|
|
41
|
+
"Agency planning mode. May update Agency plans and planning structure.",
|
|
32
42
|
mode: "primary",
|
|
33
43
|
prompt: agencyPlanPrompt,
|
|
34
44
|
permission: {
|
|
35
45
|
question: "allow",
|
|
46
|
+
bash: agencyPlanBashPermissions,
|
|
36
47
|
edit: {
|
|
37
48
|
"*": "deny",
|
|
38
49
|
"tasks/*/TASK.md": "allow",
|
|
@@ -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
|
+
}
|