@markjaquith/agency 2.44.0 → 2.46.0

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