@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 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`, and `.opencode/command/agency.md`, then `agency
86
- integration sync` to create missing files or refresh checksum-safe managed
87
- files. Customized files are reported but never overwritten. The root
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.45.0",
3
+ "version": "2.46.0",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
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(launch.skillPath)
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 and writable-checkout skills, without changing
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: "agents" | "opencode" | "opencode-command" | "opencode-plugin"
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({
@@ -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, and a managed plugin that exposes skills
76
- from the authoritative writable checkout without changing the project root.
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
+ }