@te-river/opencode-team-mode 1.7.2 → 1.7.4

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.
Files changed (178) hide show
  1. package/README.md +1008 -849
  2. package/README.zh-CN.md +905 -756
  3. package/dist/host/v2-browser-gate.d.ts +4 -1
  4. package/dist/host/v2-browser-gate.d.ts.map +1 -1
  5. package/dist/host/v2-browser-gate.js +1 -1
  6. package/dist/host/v2-browser-gate.js.map +1 -1
  7. package/dist/host/v2-capabilities.js +1 -1
  8. package/dist/host/v2-capabilities.js.map +1 -1
  9. package/dist/host/v2-events.d.ts +8 -2
  10. package/dist/host/v2-events.d.ts.map +1 -1
  11. package/dist/host/v2-events.js +16 -2
  12. package/dist/host/v2-events.js.map +1 -1
  13. package/dist/host/v2-guard.d.ts +3 -45
  14. package/dist/host/v2-guard.d.ts.map +1 -1
  15. package/dist/host/v2-guard.js +10 -73
  16. package/dist/host/v2-guard.js.map +1 -1
  17. package/dist/host/v2-offload.d.ts +12 -2
  18. package/dist/host/v2-offload.d.ts.map +1 -1
  19. package/dist/host/v2-offload.js +9 -9
  20. package/dist/host/v2-offload.js.map +1 -1
  21. package/dist/host/v2-probe-chain.d.ts +5 -1
  22. package/dist/host/v2-probe-chain.d.ts.map +1 -1
  23. package/dist/host/v2-probe-chain.js +4 -6
  24. package/dist/host/v2-probe-chain.js.map +1 -1
  25. package/dist/host/v2-probe.d.ts.map +1 -1
  26. package/dist/host/v2-probe.js +2 -1
  27. package/dist/host/v2-probe.js.map +1 -1
  28. package/dist/host/v2-prune.d.ts +5 -1
  29. package/dist/host/v2-prune.d.ts.map +1 -1
  30. package/dist/host/v2-prune.js +8 -14
  31. package/dist/host/v2-prune.js.map +1 -1
  32. package/dist/host/v2-retry.d.ts +10 -3
  33. package/dist/host/v2-retry.d.ts.map +1 -1
  34. package/dist/host/v2-retry.js +14 -16
  35. package/dist/host/v2-retry.js.map +1 -1
  36. package/dist/host/v2-session.d.ts +6 -2
  37. package/dist/host/v2-session.d.ts.map +1 -1
  38. package/dist/host/v2-session.js +7 -13
  39. package/dist/host/v2-session.js.map +1 -1
  40. package/dist/host/v2.d.ts +0 -5
  41. package/dist/host/v2.d.ts.map +1 -1
  42. package/dist/host/v2.js +32 -73
  43. package/dist/host/v2.js.map +1 -1
  44. package/dist/prompts/lead.d.ts +1 -1
  45. package/dist/prompts/lead.d.ts.map +1 -1
  46. package/dist/prompts/lead.js +47 -19
  47. package/dist/prompts/lead.js.map +1 -1
  48. package/dist/prompts/shared.d.ts +1 -1
  49. package/dist/prompts/shared.d.ts.map +1 -1
  50. package/dist/prompts/shared.js +16 -0
  51. package/dist/prompts/shared.js.map +1 -1
  52. package/dist/prompts/specialists.d.ts +1 -1
  53. package/dist/prompts/specialists.d.ts.map +1 -1
  54. package/dist/prompts/specialists.js +1 -1
  55. package/dist/tm/args-schema.d.ts +1 -1
  56. package/dist/tm/args-schema.js +6 -6
  57. package/dist/tm/args-schema.js.map +1 -1
  58. package/dist/tm/board.js +2 -2
  59. package/dist/tm/board.js.map +1 -1
  60. package/dist/tm/config-files.d.ts +41 -17
  61. package/dist/tm/config-files.d.ts.map +1 -1
  62. package/dist/tm/config-files.js +68 -73
  63. package/dist/tm/config-files.js.map +1 -1
  64. package/dist/tm/config-layers.d.ts +65 -33
  65. package/dist/tm/config-layers.d.ts.map +1 -1
  66. package/dist/tm/config-layers.js +164 -81
  67. package/dist/tm/config-layers.js.map +1 -1
  68. package/dist/tm/config-template.d.ts +32 -0
  69. package/dist/tm/config-template.d.ts.map +1 -0
  70. package/dist/tm/config-template.js +74 -0
  71. package/dist/tm/config-template.js.map +1 -0
  72. package/dist/tm/config.d.ts +113 -140
  73. package/dist/tm/config.d.ts.map +1 -1
  74. package/dist/tm/config.js +100 -200
  75. package/dist/tm/config.js.map +1 -1
  76. package/dist/tm/dispatch.d.ts +0 -9
  77. package/dist/tm/dispatch.d.ts.map +1 -1
  78. package/dist/tm/dispatch.js +14 -16
  79. package/dist/tm/dispatch.js.map +1 -1
  80. package/dist/tm/index.d.ts +12 -12
  81. package/dist/tm/index.d.ts.map +1 -1
  82. package/dist/tm/index.js +48 -26
  83. package/dist/tm/index.js.map +1 -1
  84. package/dist/tm/ledger.d.ts +7 -3
  85. package/dist/tm/ledger.d.ts.map +1 -1
  86. package/dist/tm/ledger.js +5 -4
  87. package/dist/tm/ledger.js.map +1 -1
  88. package/dist/tm/memory.js +3 -3
  89. package/dist/tm/pipelines.js +1 -1
  90. package/dist/tm/search.js +6 -6
  91. package/dist/tm/search.js.map +1 -1
  92. package/dist/tm/stats.js +2 -2
  93. package/dist/tm/stats.js.map +1 -1
  94. package/dist/tm/tools.js +2 -2
  95. package/dist/tm/webfetch.d.ts +5 -2
  96. package/dist/tm/webfetch.d.ts.map +1 -1
  97. package/dist/tm/webfetch.js +24 -18
  98. package/dist/tm/webfetch.js.map +1 -1
  99. package/docs/installation-v2.md +59 -38
  100. package/docs/installation.md +4 -4
  101. package/package.json +1 -1
  102. package/scripts/gen-v2-config.mjs +8 -7
  103. package/dist/approval-gate.d.ts +0 -205
  104. package/dist/approval-gate.d.ts.map +0 -1
  105. package/dist/approval-gate.js +0 -627
  106. package/dist/approval-gate.js.map +0 -1
  107. package/dist/host/v1.d.ts +0 -14
  108. package/dist/host/v1.d.ts.map +0 -1
  109. package/dist/host/v1.js +0 -340
  110. package/dist/host/v1.js.map +0 -1
  111. package/dist/host/v2-client.d.ts +0 -53
  112. package/dist/host/v2-client.d.ts.map +0 -1
  113. package/dist/host/v2-client.js +0 -193
  114. package/dist/host/v2-client.js.map +0 -1
  115. package/dist/host/v2-transcript.d.ts +0 -73
  116. package/dist/host/v2-transcript.d.ts.map +0 -1
  117. package/dist/host/v2-transcript.js +0 -151
  118. package/dist/host/v2-transcript.js.map +0 -1
  119. package/dist/host-hooks.d.ts +0 -126
  120. package/dist/host-hooks.d.ts.map +0 -1
  121. package/dist/host-hooks.js +0 -284
  122. package/dist/host-hooks.js.map +0 -1
  123. package/dist/tm/bash-timeout.d.ts +0 -72
  124. package/dist/tm/bash-timeout.d.ts.map +0 -1
  125. package/dist/tm/bash-timeout.js +0 -100
  126. package/dist/tm/bash-timeout.js.map +0 -1
  127. package/dist/tm/browser.d.ts +0 -642
  128. package/dist/tm/browser.d.ts.map +0 -1
  129. package/dist/tm/browser.js +0 -2836
  130. package/dist/tm/browser.js.map +0 -1
  131. package/dist/tm/ptc/budgets.d.ts +0 -59
  132. package/dist/tm/ptc/budgets.d.ts.map +0 -1
  133. package/dist/tm/ptc/budgets.js +0 -138
  134. package/dist/tm/ptc/budgets.js.map +0 -1
  135. package/dist/tm/ptc/contract.d.ts +0 -171
  136. package/dist/tm/ptc/contract.d.ts.map +0 -1
  137. package/dist/tm/ptc/contract.js +0 -85
  138. package/dist/tm/ptc/contract.js.map +0 -1
  139. package/dist/tm/ptc/driver.d.ts +0 -40
  140. package/dist/tm/ptc/driver.d.ts.map +0 -1
  141. package/dist/tm/ptc/driver.js +0 -213
  142. package/dist/tm/ptc/driver.js.map +0 -1
  143. package/dist/tm/ptc/engines.d.ts +0 -43
  144. package/dist/tm/ptc/engines.d.ts.map +0 -1
  145. package/dist/tm/ptc/engines.js +0 -454
  146. package/dist/tm/ptc/engines.js.map +0 -1
  147. package/dist/tm/ptc/gate.d.ts +0 -28
  148. package/dist/tm/ptc/gate.d.ts.map +0 -1
  149. package/dist/tm/ptc/gate.js +0 -130
  150. package/dist/tm/ptc/gate.js.map +0 -1
  151. package/dist/tm/ptc/index.d.ts +0 -19
  152. package/dist/tm/ptc/index.d.ts.map +0 -1
  153. package/dist/tm/ptc/index.js +0 -15
  154. package/dist/tm/ptc/index.js.map +0 -1
  155. package/dist/tm/ptc/pscan.d.ts +0 -30
  156. package/dist/tm/ptc/pscan.d.ts.map +0 -1
  157. package/dist/tm/ptc/pscan.js +0 -341
  158. package/dist/tm/ptc/pscan.js.map +0 -1
  159. package/dist/tm/ptc/summary.d.ts +0 -17
  160. package/dist/tm/ptc/summary.d.ts.map +0 -1
  161. package/dist/tm/ptc/summary.js +0 -134
  162. package/dist/tm/ptc/summary.js.map +0 -1
  163. package/dist/tm/ptc/tool.d.ts +0 -66
  164. package/dist/tm/ptc/tool.d.ts.map +0 -1
  165. package/dist/tm/ptc/tool.js +0 -231
  166. package/dist/tm/ptc/tool.js.map +0 -1
  167. package/dist/tm/ptc.d.ts +0 -298
  168. package/dist/tm/ptc.d.ts.map +0 -1
  169. package/dist/tm/ptc.js +0 -865
  170. package/dist/tm/ptc.js.map +0 -1
  171. package/dist/tm/pty.d.ts +0 -70
  172. package/dist/tm/pty.d.ts.map +0 -1
  173. package/dist/tm/pty.js +0 -245
  174. package/dist/tm/pty.js.map +0 -1
  175. package/dist/tool-coerce.d.ts +0 -24
  176. package/dist/tool-coerce.d.ts.map +0 -1
  177. package/dist/tool-coerce.js +0 -63
  178. package/dist/tool-coerce.js.map +0 -1
package/README.md CHANGED
@@ -1,849 +1,1008 @@
1
- # OpenCode TeamMode
2
-
3
- **[English](./README.md)** | **[中文](./README.zh-CN.md)**
4
-
5
- [![npm version](https://img.shields.io/npm/v/@te-river/opencode-team-mode.svg)](https://www.npmjs.com/package/@te-river/opencode-team-mode)
6
- [![npm downloads](https://img.shields.io/npm/dm/@te-river/opencode-team-mode.svg)](https://www.npmjs.com/package/@te-river/opencode-team-mode)
7
- [![license](https://img.shields.io/npm/l/@te-river/opencode-team-mode.svg)](./LICENSE)
8
-
9
- > 🤝 **Your OpenCode just hired a team.**
10
- >
11
- > Six specialized agents — a Lead, an Architect, an Implementer, a Reviewer, a Tester and a Researcher — with governed tools, structured handoffs, and a plan-first approval gate. One plugin, zero config files to copy.
12
-
13
- > 💡 **Best for medium-to-large projects.** The governance layer (approval gate, context offload, tool allowlists) is an asset once a codebase has real surface area — and mostly overhead on tiny scripts and one-off questions. Use it where the work is.
14
-
15
- ---
16
-
17
- ## TL;DR — skip the docs
18
-
19
- > **The lazy path:** paste this to any coding agent and let it do the work:
20
- >
21
- > ```text
22
- > Install the OpenCode plugin @te-river/opencode-team-mode. This package supports **OpenCode 2.x
23
- > only** — the 1.18.x personality was removed in the 1.7.0 line:
24
- > 2.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation-v2.md
25
- > (a 2.x plugin cannot create an agent, so the six roles and six `/team-*` commands come
26
- > from a generator step in that guide — that step is not optional.) Then verify the install
27
- > using the checks in that guide.
28
- > ```
29
- >
30
- > **The one command to remember afterwards:** `/team-run <task>` — the Team Lead
31
- > plans it, shows you the plan, waits for your approval, then runs the whole team.
32
-
33
- Everything else below is detail. When you're ready for it, here's the map:
34
-
35
- **[Why](#-why-teammode) · [The team](#-the-team) · [Install](#-install) · [Usage](#-usage) · [Tools & security](#-governed-tools--security) · [Search](#-web-search-that-actually-works-in-china) · [Config](#️-configuration) · [How it works](#-how-the-team-works) · [FAQ](#-faq) · [Uninstall](#-uninstall)**
36
-
37
- ---
38
-
39
- ## 🤔 Why TeamMode?
40
-
41
- Because a single agent doing everything is how you get: a context window stuffed
42
- with 5000-line file dumps, twenty round-trips to `bash` for what one script could
43
- do, sub-agents that silently read `.env`, and web "research" that is really just
44
- the model's imagination.
45
-
46
- TeamMode's answer to each:
47
-
48
- | Pain | TeamMode's answer |
49
- |---|---|
50
- | 🔥 **Context flooding** | Every governed tool output over its content-class offload threshold (prose 4000 / data 2000 tokens, CJK-aware) is offloaded to a local run store and replaced by an 80-token preview + an HMAC handle. The agent pages through what it needs — the window never drowns. |
51
- | 🐌 **Round-trip overhead** | The host's own `execute` (Code Mode): the agent writes ONE program that makes N governed calls in a single turn. Zero LLM round-trips during the run. |
52
- | 🕳️ **Silent side effects** | R6/R2 approval gate: env-var reads and dangerous ops route through OpenCode's official confirmation dialog, auto-rejected after 1 unanswered minute (default). The plugin never approves on its own — it only ever rejects. |
53
- | 🌫️ **Hallucinated research** | Web access is a two-role grant with an allowlisted, governed tool chain. A fact that couldn't be fetched is reported as a gap — never fabricated. |
54
- | 🧭 **Walls of text** | Replies are steered into the shape the host renders fastest: a markdown table for per-file / per-case / per-finding results, fenced code for diffs and configs, a browser screenshot attached as an inline image only when you ask for one. The renderer's supported set is measured rather than assumed, and the prompts carry the negative half of that measurement — footnotes, `==highlight==`, a bare `---` rule and `$…$` math reach you as literal text, so agents are told not to use them, while `mermaid` diagrams are offered because this host does draw them. |
55
- | 🎯 **Goal drift** | The lead opens with `GOAL:` in your own words plus checkable `ACCEPTANCE:` criteria, and the run does not end while a criterion lacks evidence — the only legitimate stops are named (blocked on you, or provably unachievable). When a round settles with items still open on the host's todo list, `tm_join` says 目标未达成 and lists them, and the goal is carried through context compaction so a summarized transcript cannot redefine it. |
56
-
57
- | 🗣️ **Replies in a language you never chose** | The governed tools answer in Chinese, and an agent left to its own devices mirrors that straight back at you. Every role now carries a reply-language rule: your own language wins, and a Chinese string is quoted verbatim only where it IS the evidence (a close verdict, a refusal line) — translating a verdict is how an unchecked claim starts looking checked. |
58
- | ⏱️ **Rounds spent for the sake of looking careful** | 效率至上 is written into the lead and all five specialists: one wide call instead of three narrow ones, independent calls in the same round, ≥3 probes collapsed into one `execute` (Code Mode) program, no re-running a check to watch it pass again — with the boundary stated too: efficiency never buys its way out of the evidence rule, because an unverified "done" costs you the round *and* the bug. |
59
-
60
- And the workflow discipline underneath: deterministic routing, a ≤30-line plan
61
- you approve before ≥2 dispatches execute, structured `STATUS/CHANGES/FINDINGS/
62
- EVIDENCE/HANDOFF` replies between agents, and static verification (build /
63
- typecheck / tests) instead of vibes.
64
-
65
- That discipline is also why the recommendation is **medium-to-large
66
- projects**: on a two-file script the team simply has less to govern.
67
-
68
- ---
69
-
70
- ## 👥 The team
71
-
72
- | Agent | Role | When to use |
73
- |---|---|---|
74
- | 🎯 **Team Lead** (`@team`) | Orchestrator | Complex tasks that need planning + multi-step execution |
75
- | 🏗️ **Architect** | System designer | Design docs, module structure, API contracts |
76
- | 💻 **Implementer** | Code writer | Building features, writing production code |
77
- | 🔍 **Reviewer** | Dimension-focused auditor | Single-dimension review by default; 3 in parallel only for high-risk changes |
78
- | 🧪 **Tester** | Test engineer | Tests with real edge cases; static verification (build / typecheck / lint); governed UI verification via the host's native `browser_*` tools |
79
- | 🔎 **Researcher** | Knowledge finder | Local repo first, then the web — one of the two network roles (with the Lead) |
80
-
81
- Out of the box, **Team is your default agent** — new chats open straight into the
82
- orchestrator (opt-out in [Configuration](#-configuration)).
83
-
84
- ---
85
-
86
- ## 📦 Install
87
-
88
- ### Option 1 — Let your agent do it (recommended)
89
-
90
- Copy this into any coding agent — it will edit your config, restart-remind you, and verify:
91
-
92
- ```text
93
- Install the OpenCode plugin @te-river/opencode-team-mode. This package supports **OpenCode 2.x
94
- only** — the 1.18.x personality was removed in the 1.7.0 line:
95
- 2.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation-v2.md
96
- (a 2.x plugin cannot create an agent, so the six roles and six `/team-*` commands come from
97
- a generator step in that guide — that step is not optional.)
98
- (If a URL is unreachable — common on mainland-China networks — retry with
99
- the mirror prefix: https://ghproxy.net/ + the same path.)
100
- Then verify the install using the checks in that guide.
101
- ```
102
-
103
- (The guide is the complete manual procedure — config file locations, plugin
104
- entry, restart, verification, update and uninstall. Your agent reads it and
105
- executes it faithfully; there is nothing else it needs.)
106
-
107
- ### Option 2 — One-line script
108
-
109
- macOS / Linux (bash):
110
-
111
- ```bash
112
- curl -fsSL https://ghproxy.net/https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/scripts/install.sh | bash
113
- ```
114
-
115
- Windows (PowerShell):
116
-
117
- ```powershell
118
- irm https://ghproxy.net/https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/scripts/install.ps1 | iex
119
- ```
120
-
121
- ### Option 3 — Manual
122
-
123
- Add the plugin to your `opencode.jsonc`:
124
-
125
- ```jsonc
126
- {
127
- "$schema": "https://opencode.ai/config.json",
128
- "plugin": [
129
- "@te-river/opencode-team-mode@latest"
130
- ]
131
- }
132
- ```
133
-
134
- OpenCode installs the plugin on next startup.
135
-
136
- > **On OpenCode 2.x the key is `plugins` (plural)** — a 2.x host never reads the singular
137
- > `plugin` key, and it installs the package itself from the entry at startup. See
138
- > [docs/installation-v2.md](./docs/installation-v2.md) for the 2.x flow (a plugin cannot
139
- > create an agent there, so the six roles and six commands are generated config files);
140
- > `docs/research/plugin-loader-contract.md` has the host's own loader code.
141
-
142
- ### ⚠️ Read this once, save yourself an hour later
143
-
144
- - **Restart to activate.** After touching `opencode.json`, fully quit and restart OpenCode (Desktop: quit from tray, not just the window).
145
- - **Plugin updates: re-run the installer.** It is idempotent — a re-run re-patches the config (no-op when present), purges the stale plugin cache, and re-resolves any npm-installed copy. This exists because OpenCode caches plugins by spec string and does NOT re-resolve `@latest` when a new version publishes (upstream limitation). Manual recipe, if you prefer:
146
-
147
- | OS | Purge command — recursive, covers BOTH cache layouts |
148
- |---|---|
149
- | macOS / Linux | `find ~/.cache/opencode/packages -type d -name '*opencode-team-mode*' -prune -exec rm -rf {} +` |
150
- | Windows (PowerShell) | `foreach ($r in "$HOME\.cache\opencode\packages", "$env:LOCALAPPDATA\opencode\cache\packages") { if (Test-Path $r) { Get-ChildItem $r -Directory -Recurse -Filter '*opencode-team-mode*' -EA SilentlyContinue | Sort-Object { $_.FullName.Length } | Remove-Item -Recurse -Force -EA SilentlyContinue } }` |
151
-
152
- ⚠️ OpenCode loads the plugin from `~/.cache/opencode/packages/` — possibly nested inside the scoped `@te-river/` directory — **not** from `~/.config/opencode/node_modules`, which is why the delete must recurse (a top-level-only `rm -rf` silently misses the scoped copy).
153
-
154
- If you also npm-installed the plugin into `~/.config/opencode`, its package-lock pins the version — run `npm install @te-river/opencode-team-mode@latest` there too. Full recipe (including an agent-driven update prompt): [installation guide, Updating](./docs/installation.md).
155
- - **Prerequisites:** [OpenCode](https://opencode.ai) (Desktop or CLI) and Node ≥ 18.
156
-
157
- ### 🖥️ What the installer changes on your machine
158
-
159
- Three things, all outside this repo — listed because a plugin that edits your
160
- environment without saying so is not one you can trust.
161
-
162
- | What | Where it lands | How long it stays |
163
- |---|---|---|
164
- | A plugin entry in the OpenCode config | `~/.config/opencode/opencode.jsonc` (created if missing; an existing `opencode.json` is **migrated into** the `.jsonc`, the original file left untouched) | until you delete the line |
165
- | A purged plugin cache | `~/.cache/opencode/packages/*opencode-team-mode*` (including the nested `node_modules` copy that actually runs) | re-downloaded on next start — it is a cache, nothing to restore |
166
- | **A user-level environment variable** | `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` | **Windows: `setx` writes it into your user profile — it survives reboot and is visible to EVERY program you start afterwards, not just OpenCode.** macOS `launchctl setenv` / Linux `systemctl --user set-environment`: this login session only (re-run the installer after a reboot) |
167
-
168
- Why that third one is there: `task { background: true }` is the only sub-agent
169
- the OpenCode interface can **show** you — a card linking to the live child
170
- session, which you can stop. The host turns it on through a flag on **its own
171
- process**, and a plugin cannot set flags in the process that loads it, so the
172
- flag has to come from the environment. Without it nothing breaks: `task` simply
173
- blocks the lead and its card stays non-background.
174
-
175
- Undo it:
176
-
177
- ```powershell
178
- # Windows — remove it from the user profile
179
- [Environment]::SetEnvironmentVariable("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS", $null, "User")
180
- ```
181
-
182
- ```bash
183
- launchctl unsetenv OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS # macOS
184
- systemctl --user unset-environment OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS # Linux
185
- ```
186
-
187
- Or skip it at install time, and the installer touches only the two files above:
188
-
189
- ```powershell
190
- .\install.ps1 -NoBackgroundSubagents # or $env:TEAMMODE_SKIP_BACKGROUND_SUBAGENTS="1"
191
- ```
192
-
193
- ```bash
194
- TEAMMODE_SKIP_BACKGROUND_SUBAGENTS=1 bash install.sh
195
- ```
196
-
197
- Either way the installer prints what it changed, and reads the value back
198
- instead of trusting its own exit code.
199
-
200
- ### Verify
201
-
202
- Restart, open the agent picker, and look for **team, architect, implementer,
203
- reviewer, tester, researcher**. That's it — the team is hired.
204
-
205
- ---
206
-
207
- ## 📖 Usage
208
-
209
- ### Slash commands
210
-
211
- | Command | Agent | Description |
212
- |---|---|---|
213
- | `/team-plan <task>` | Architect | Implementation plan with architecture, file manifest, task breakdown |
214
- | `/team-implement <task>` | Implementer | Production code for a feature or task |
215
- | `/team-review [scope]` | Reviewer | Audit for bugs, security issues, quality problems |
216
- | `/team-test [scope]` | Tester | Comprehensive tests with edge-case coverage |
217
- | `/team-research <topic>` | Researcher | Local repo first; web via the governed tools |
218
- | `/team-run <task>` | Team Lead | **The full workflow** — plan → approval → orchestration → verification |
219
-
220
- Or `@`-mention agents directly: `@team`, `@architect`, `@implementer`,
221
- `@reviewer`, `@tester`, `@researcher`.
222
-
223
- ### What a run feels like
224
-
225
- Team is the default agent out of the box, so you just type into a fresh chat:
226
-
227
- > **You:** Add token-bucket rate limiting to our Express API — 100 requests per minute per user, return 429 when exceeded. Don't touch anything under `src/legacy/`.
228
-
229
- ```text
230
- team Route: product behavior change → implementer → tester → reviewer
231
- (3 dispatches → approval gate applies)
232
-
233
- PLAN
234
- Goal: token-bucket limiter, 100 req/min/user, 429 + Retry-After
235
- Scope: src/middleware/rateLimit.ts (new), src/app.ts (+3 lines)
236
- Pipeline: implementer → tester → reviewer (correctness)
237
- Assumption: in-memory bucket, not Redis — say the word to change
238
- Boundary: src/legacy/** untouched
239
- Approve to proceed?
240
-
241
- you approved, go
242
-
243
- team ▸ @implementer — STATUS: done
244
- CHANGES: src/middleware/rateLimit.ts (new) · src/app.ts (+3)
245
- EVIDENCE: tsc clean
246
-
247
- ▸ @tester — STATUS: done · VERDICT: pass (14/14)
248
- FINDINGS: burst edge, window expiry, concurrent refill covered;
249
- src/legacy/** byte-identical
250
-
251
- ▸ @reviewer — STATUS: done · VERDICT: approve (correctness)
252
-
253
- team Done. rateLimit.ts (new) · app.ts (+3) · 14 tests green
254
- review: approve · assumption: in-memory only · src/legacy/ untouched
255
- ```
256
-
257
- You typed the task and four words. The plan was a routing-table lookup,
258
- execution waited for your approval, and every handoff between agents traveled
259
- as a structured skeleton — nothing landed in your repo, nothing was guessed.
260
-
261
- ---
262
-
263
- ## 🧰 Governed tools & security
264
-
265
- Every tool TeamMode adds runs under ONE governance pipeline: outputs above
266
- the offload threshold never enter the context window — the boundary is
267
- content-class aware (`TM_OFFLOAD_THRESHOLD_TEXT` = 4000 for prose:
268
- text/log/markdown; `TM_OFFLOAD_THRESHOLD_DATA` = 2000 for json/csv/code/
269
- binary; an unknown class falls back to the global `TM_OFFLOAD_THRESHOLD`) —
270
- and oversized results are offloaded to a run store and replaced by a
271
- content-aware preview plus an HMAC-signed handle that the agent pages
272
- through with `tm_fetch` when it genuinely needs the payload.
273
-
274
- | Tool | What it does | Roles |
275
- |---|---|---|
276
- | `tm_fetch` | Paged handle retrieval for offloaded results (JSON handles take a `fields` dot-path projection — a deliberately small jq subset like `items[].name`) | all six agents |
277
- | `tm_memory` | Session + project + global memory store (Markdown + frontmatter): add / search / list / forget / compact | all six agents |
278
- | `tm_board_write` | **The blackboard's write side**: places ONE new Markdown file at `<board-root>/<session-key>/<task-slug>/NN-<role>-<topic>[-rN].md` and chooses the name itself — a revision is a new round-suffixed file, never an overwrite, and the reply carries the path plus the byte count, never the content. It exists because the board used to need a file tool, and `architect` / `researcher` own none (no `write`, no `edit`, no `bash` even to stamp the session folder), so every oversized deliverable from those roles came back as `BLACKBOARD WRITE FAILED` plus the whole document pasted inline — the reply shape this team mandates was un-followable exactly where it mattered. Scope is enforced rather than asked: segments sanitized, target realpath-verified against the board root (a symlinked task dir is refused), the name always ends in `.md` so no `.env`/rc file can be produced, caps via `TM_BOARD_MAX_CHARS` + a per-session file limit | All six agents |
279
- | `tm_search` | Multi-engine web search with extracted, deduplicated, RRF-fused hit lists | Lead + Researcher |
280
- | `tm_webfetch` | Single governed GET of an allowlisted page (search pages auto-extracted). Manual redirects, re-checked per hop, and a refusal names the WHOLE chain (`跳转链: a → b(停在第 2 跳)`) — an allowlisted shortener that bounces off-site used to report only the off-site host, which read as "that site will not fetch" and sent the agent back to retry the entry URL it had just watched fail. A 429/503 carries its own `Retry-After` when the server sends delta-seconds (an HTTP-date is deliberately not laundered into a countdown), so "come back later" never looks like "no content here". The page GET also asks for Markdown first (`Accept: text/markdown,…`) — measured on `learn.microsoft.com`: 60,778 B of HTML becomes 11,449 B of Markdown, and every other host tried returns the same document either way, so the preference is free where it is ignored | Lead + Researcher |
281
- | `tm_ledger` | **The lead's task ledger** (`add` / `doing` / `done` / `blocked` / `list`), stored in the host's own `ctx.storage` per session — the landing spot for the LEDGER rule, since OpenCode 2.x gives a plugin no `todowrite`. A repeated ask is ONE item, an id that matches two is refused with both printed, `blocked` carries the reason, and a write that did not reach storage is reported as a failure rather than as recorded | Lead only |
282
- | `tm_join` | **Sub-agent collection** — there is no plugin-side dispatcher any more (`tm_dispatch` is removed: a child a plugin creates is a session the user can neither open nor stop from the interface). Delegation goes through the host's own `task` / `task { background: true }`, and `tm_join` is the read side: a status snapshot, a bounded `waitMs`, `cancel:true` to stop a runaway, and `claimNamedChild` so `tm_join { ids: ["ses_…"] }` pulls one child's WHOLE reply back through the offload pipeline (handle + ≤80-token preview) instead of kilotokens inline. It also rebuilds its registry from the host session tree after a restart, so leftover children are 接管 rather than lost. **On 2.x it also registers the children the host's own `subagent` tool created** (the ack's `metadata.sessionID` is the seam), so a background dispatch is never reported as "nothing to collect" while the user watches it run — and such a row says where its reply actually arrives (the host's injected message) and whether it settled by event or by inference. **Stopping one is a real call:** `cancel: true` goes to the host's `POST /api/session/{id}/interrupt` on 2.x (`tm_join { ids: ["ses_…"], cancel: true }` stops ONE named child; without `ids` it stops every still-running one). The host's own contract is `interrupted=true` for an active execution and `false` for the idle no-op, so the answer comes back as **five distinct verdicts** — 已由宿主中断 / 空闲未中断(it wasn't running: our row was stale, not failed)/ 未确认(the call worked, no boolean came back)/ 无中断缝 / 被宿主拒绝(with the host's reason) — counted in the reply and written to the trajectory (`stop_tried` / `stop_confirmed` / `stop_refused` / `stop_unknown`, readable via `tm_stats`). It will not summarise five different facts as one "已取消", and `resume` is never sent, because resuming pending steering is the opposite of cancelling. *(On `main` only — the published 1.6.1 still answers 宿主无 abort 接口 on 2.x.)* | Lead only |
283
- | `tm_stats` | **The plugin reads its own trajectory back**: tokens kept out of the context window by offloading (net of the preview that arrived), seconds saved by dispatch overlap (serial cost minus the wall window the children actually used), governance counts (blocked subresources, clamped shell timeouts, cache hits, redactions) — plus the **host capability matrix** (`已验证/存在未用/待观察/缺失/需人眼` per host surface). Read-only over files this plugin wrote; run it first after an OpenCode upgrade. `{ recent: 20 }` appends a call-by-call recap — handle + payload path for every offloaded result, which is how you see what a governed tool actually returned (the host gives plugin tools no expandable card) | All agents |
284
-
285
- > **The v1 (1.18.x) personality was removed in full for the 1.7.0 line.** The package
286
- > exports `{id, setup}` and nothing else, so `tm_read` / `tm_grep` / `tm_bash` /
287
- > `tm_ptc_run` / `tm_pty` are gone with it — the host's own `read` / `grep` / `glob` /
288
- > `shell` and its `execute` (Code Mode) do that work now. The governance did NOT
289
- > disappear with them: an oversized native result is offloaded through
290
- > `tool.execute.after` (measured: `shell` 12,902 tokens arriving as a 78-token
291
- > preview), and the address red line plus R6's per-command classification ride the
292
- > host's `permission.evaluate`. `tm_ledger` is the lead's list where the host gives a
293
- > plugin no `todowrite`.
294
- > **Nothing we change reaches outside Team.** Every v2 hook fires for every session on
295
- > the host, so the tool-surface trim, the 0.2 temperature, the blackboard note, the offload
296
- > of native results, the R6/address strictening and the forced-background dispatch all ask
297
- > "is this one of our six roles?" first — `build`, `plan` and your own agents stay as a
298
- > fresh install leaves them. Calls whose owner the host did not tell us are left alone too,
299
- > and counted (`tm_stats` prints `作用域:我们 · 他人 · 未判定`).
300
- > The installer writes the six
301
- > role prompts, which name `read` / `grep` / `shell` instead of the removed aliases.
302
-
303
- > **On OpenCode 2.x, interactive browsing is the host's, and it stays ours.** The
304
- > desktop renders a browser in its side panel, and that panel attaches to the
305
- > server's own browser service — a plugin cannot register a page of its own into it
306
- > (forensics: `docs/research/browser-pane.md`). So the three roles with network
307
- > grants (lead, researcher, tester) are pointed at the host's `browser_*` tools
308
- > — the self-built `tm_browser` was removed in 1.7.0, so the host's catalog is the
309
- > only browser. On a host with no native browser catalog (CLI, standalone) there is
310
- > now no browser at all, and the agent reports that gap rather than simulating one.
311
- > Handing browsing over did not mean handing
312
- > governance over: `permission.evaluate` was observed NOT firing for `browser_*`, so
313
- > our gate sits on `tool.execute.before` instead — it classifies the URL of every
314
- > navigate/open, the path of every `browser_preview`, and every browser URL named
315
- > inside an `execute` program, refuses the ones out of policy, and if the host runs a
316
- > refused call anyway it replaces the page with the same refusal, so no
317
- > out-of-policy content reaches the context, the store or the trajectory. What we
318
- > cannot do is un-make a request the host already performed, and the reply says so
319
- > rather than claiming a block; `tm_stats` prints the two numbers apart (refused /
320
- > leaked past the refusal). One exception to the offload rule lives here: a
321
- > `browser_snapshot` is an addressing table, not a document, so it is CAPPED (every
322
- > `[ref=…]` line kept, static text dropped, budget `TM_NATIVE_SNAPSHOT_MAX_TOKENS`
323
- > default 1 200) rather than replaced by a
324
- > handle. Measured on a 261-ref page: a head cut keeps 118 refs, this keeps 261 of
325
- > them using 1 044 of 11 326 tokens.
326
-
327
- > **Fixed tool priority ladder (every task): ① the user's own MCP/plugin
328
- > tools → ② TeamMode governed tools (`tm_*`) → ③ the model's own reasoning.**
329
- > It doubles as the fallback chain: when a tool errors (no browser on this
330
- > host, blocked host), the agent says so and drops to the next rung — and
331
- > rung ③ is where a missing capability gets reported, never fabricated.
332
- > One stated exception: on the **web** channel `tm_search` / `tm_webfetch` come
333
- > first, because that path is the only one carrying the
334
- > domain allowlist, the per-request confirmation dialog and the R6 red lines.
335
-
336
- > **What the host UI cannot show you.** OpenCode renders a *plugin* tool call
337
- > as a one-line card with no expandable body — its tool-renderer registry
338
- > holds only the built-in tool names, and an extension cannot add to it. The
339
- > governed output still exists: anything over the offload boundary is written
340
- > to disk as a handle, and `tm_stats { recent: 20 }` prints the call-by-call
341
- > recap with those handles and file paths, which you can open. Ask your agent
342
- > for it — "what did that tool actually return?"
343
-
344
- > **Delegation goes through the host's `task` — one channel, two shapes.**
345
- > OpenCode's own `task` tool is the only sub-agent the interface can SHOW you:
346
- > its card links to the live child session, and the user can stop it. Add
347
- > `background: true` and it is also non-blocking — the host wakes your lead when
348
- > the child finishes. That flag is experimental, so you turn it on yourself:
349
- >
350
- > ```
351
- > OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
352
- > ```
353
- >
354
- > (set it for the app process — `setx` on Windows, or launch from a shell that
355
- > exports it — then restart OpenCode; the installers do it for you, and that is
356
- > a **persistent user-scope write**, so see
357
- > [What the installer changes on your machine](#-what-the-installer-changes-on-your-machine)
358
- > for the exact value, its lifetime and the undo command). TeamMode
359
- > then keeps that channel inside your token budget: the injected full reply is
360
- > replaced by a preview plus a pointer, and nothing is copied to disk
361
- > (`TM_TASK_OFFLOAD=off` restores the host's verbatim text). Cost of the host
362
- > path, stated plainly: each finished background task wakes the lead and costs a
363
- > turn. `tm_join` is the read side that stays — it pulls a named child's whole
364
- > reply back through the offload pipeline, aborts a runaway, and rebuilds its
365
- > registry from the host session tree after a restart.
366
- >
367
- > There is deliberately **no plugin-side dispatcher**: an earlier `tm_dispatch`
368
- > created children the user could neither open from a card nor stop from the
369
- > interface, which is a worse trade than the batching it bought. Remove it,
370
- > and nothing breaks — the host's `task` was always the visible path.
371
-
372
- All governed tools are **parallel-safe**: the host may run a batch of
373
- `tm_search` / `tm_webfetch` / `tm_fetch` calls concurrently — each call gets
374
- its own step id and its own payload, and nothing cross-contaminates
375
- (proven by the parallel test suite).
376
-
377
- ### Context governance: offload, handles, previews
378
-
379
- Large tool outputs are context cost's main driver — every step re-sends the
380
- whole window. So results above the content-class offload boundary (prose →
381
- `TM_OFFLOAD_THRESHOLD_TEXT`, structured data → `TM_OFFLOAD_THRESHOLD_DATA`,
382
- unknown → global `TM_OFFLOAD_THRESHOLD`) are written to a local run store
383
- (`<repo>/.git/opencode-team/`, never your working tree) and replaced by a
384
- handle with a content-aware preview: JSON keys / CSV header + shape / log
385
- ERROR×N stats / code signatures / binary metadata, hard-capped at 80 tokens.
386
- When the agent actually needs the payload, it pages through with `tm_fetch`
387
- using an HMAC-signed, run-scoped, expiring handle — and on a JSON handle it
388
- can ask for a `fields` dot-path projection instead (a deliberately small jq
389
- subset: `items[].name`, `[].stargazers_count`), so a big API dump narrows to
390
- just the values needed without the raw body ever entering the window.
391
- The host's own `shell` runs read-only probes under its permission rules, and failures
392
- come back as structured errors instead of raw dumps.
393
-
394
- ### Session + project + global memory (tm_memory)
395
-
396
- Durable facts — build commands, environment quirks, architecture decisions,
397
- your conventions — live as human-editable Markdown with frontmatter, in
398
- THREE tiers:
399
-
400
- - **`session`**: this conversation's transients only — in-process, TTL-swept
401
- (`TM_MEMORY_SESSION_TTL_MIN`, default 240 min), invisible to other
402
- sessions; ephemeral unless `TM_MEMORY_SESSION_PERSIST=1` writes them under
403
- `memories/sessions/<sid>/`.
404
- - **`project`** (default): `<repo>/.git/opencode-team/memories/…` — per checkout, git-adjacent. Facts about THIS repo: build commands, environment quirks, architecture decisions.
405
- - **`global`**: `~/.opencode-team/memories/global/` (override `TM_MEMORY_GLOBAL_DIR`) — **follows you across ALL projects**. User-level conventions: preferred package manager, commit style, tooling habits.
406
-
407
- Actions: `add` / `search` (deterministic keyword scoring) / `list` / `forget` /
408
- `compact`; 4000 chars per memory. `search` walks ALL tiers with **session >
409
- project > global precedence** — project entries get a +2 near-tie weight and a
410
- same-title higher-layer twin shadows the lower one (it never surfaces).
411
- Near-duplicates never pile up: an `add` that hits an existing entry in the same
412
- tier and category (dedup key, or title+keywords Jaccard ≥ 0.6) **folds into
413
- it** — new content wins, keywords union, the old slug moves to `supersedes:`,
414
- and the answer says 已合并 (that is normal; don't re-add under a variant title).
415
- When a tier reaches `TM_MEMORY_MAX_ENTRIES` (200 per scope) the add fails on
416
- purpose: run `compact` first — it dry-runs the merge plan by default, and
417
- re-running with `apply:true` performs it after copying every original to a
418
- timestamped `.compact-backup` tree (the rollback path). Entries older than
419
- `TM_MEMORY_STALE_DAYS` (30) surface tagged `[stale Nd]` in search. Agents
420
- are prompted to search before assuming conventions and to save hard-won
421
- facts for the next conversation.
422
-
423
- ### 🌐 Web search that actually works (in China)
424
-
425
- `tm_search` is the open-ended-lookup front: **one call, one query, clean
426
- results**. The engine URL is built for you, fetched through the governed
427
- pipeline, and collapsed into a numbered title+URL hit list — the agent never
428
- sees raw SERP chrome.
429
-
430
- | Engine | Notes |
431
- |---|---|
432
- | `auto` (default) | Classifies the query, fans the matching engines out **in parallel** (every route has ≥2 legs), dedupes by host+path and fuses them with weighted RRF into a top-10 list tagged with each hit's source engine(s). Ranking is scaled by real query-token overlap (floor `TM_SEARCH_RELEVANCE_FLOOR`), so a trusted engine's off-topic junk no longer outranks another engine's best hit. Routing: errors / camelCase APIs → `stackoverflow`+`github`+`bing`; dev-ecosystem (releases, frameworks, open source) → `hn`+`github`+`npm`; Chinese → `bing`+`moegirl`+`stackoverflow`+`hn`; other → `bing`+`stackoverflow`+`hn`+`github`. Pin another default via `TM_SEARCH_DEFAULT_ENGINE` |
433
- | `bing` | cn.bing.com — the only live CN HTML SERP; multi-word CJK queries get their phrase boundary protected (quoted) so markup shuffle can't split the result list |
434
- | `stackoverflow` | api.stackexchange.com question search (no key, 300/day/IP) → numbered questions with composite snippets; `auto` tracks the quota and swaps in `bing` once spent |
435
- | `hn` | Hacker News via Algolia API (no key) → story titles + snippets with direct article URLs |
436
- | `bilibili` | video search |
437
- | `moegirl` | MediaWiki search API — entry titles + snippets, structured |
438
- | `npm` | registry search → name@version + description, structured |
439
- | `github` | repo search API → stars + description, structured; `org:` / `user:` / `stars:` / `language:` qualifiers fold into the query (e.g. `vector db stars:>500 language:rust`) |
440
-
441
- All seven engines are reachable from mainland China **without API keys**, and
442
- every one of them sits on the seeded domain allowlist. The previous CN HTML
443
- SERPs (`sogou` / `so` / `baidu` / `bing-int`) were **removed** — a live
444
- benchmark (2026-09-14) showed them serving anti-bot shells or 100% empty
445
- results; they are not even manually selectable. On an empty result,
446
- the error names the alternative engines instead of leaving the agent stuck.
447
- Two more channels complete the surface:
448
-
449
- - `tm_webfetch` — a known URL, one governed GET. Search-engine pages it
450
- fetches are auto-extracted to hit lists too. JSON endpoints like
451
- `registry.npmjs.org/<pkg>/latest` pass through untouched.
452
- - **Interactive browsing is the host's own `browser_*` tools** (`browser_tabs_open`
453
- · `browser_navigate` · `browser_snapshot` · `browser_click` · `browser_evaluate`
454
- …). The self-built `tm_browser` was **removed in 1.7.0** (a breaking change):
455
- the desktop renders a browser in its side panel and that panel attaches to the
456
- server's own browser service, so a plugin cannot register a page of its own
457
- (forensics: `docs/research/browser-pane.md`). The host catalog is policed by
458
- `src/host/v2-browser-gate.ts` at `execute.before` — the URL of every navigate/open,
459
- the path of every `browser_preview`, and every browser URL named inside an
460
- `execute` program are classified against the address red lines and the env-file
461
- rule, and a refused call the host runs anyway has its page replaced with the same
462
- refusal, so no out-of-policy content reaches the context, the store or the
463
- trajectory. Calling convention differs from the deleted tool: `evaluate` takes
464
- `{tabID, script}` where `script` is an **expression** (not a function source) and
465
- the return value must be a scalar you `JSON.stringify` yourself; snapshot tokens
466
- read `@e8 [link]`. **Known cost, accepted:** on a host with no native browser
467
- catalog (CLI / standalone) there is now no browser at all — the agent reports that
468
- gap rather than simulating one.
469
-
470
- When a fetch still returns **403 after the real-Chrome headers**, the error
471
- is a DIRECTIVE: the gate is JS-challenge / TLS-fingerprint based and only a
472
- real browser passes — the agent is told to open that URL with the host's
473
- native `browser_navigate` (and read it with `browser_snapshot`). Search hit lists also
474
- filter known noise: engine-internal wrappers (`so.com/link?`, `ai.so.com`)
475
- and same-name-different-site domains (`maimai.cn` 脉脉 vs the maimai DX
476
- game) never ride along — extend the hit blacklist with `TM_HIT_BLACKLIST`.
477
-
478
- Seeded allowlist (all three web tools; 23 hosts — baidu/moegirl/bilibili are
479
- PARENT domains, so every sibling subdomain — baike.baidu.com,
480
- mzh.moegirl.org.cn, space.bilibili.com — is covered):
481
- `baidu.com`, `moegirl.org.cn`, `bilibili.com`, `www.sogou.com`, `www.so.com`,
482
- `cn.bing.com`, `www.bing.com`, `zhihu.com`, `juejin.cn`, `csdn.net`,
483
- `cnblogs.com`, `gitee.com`, `github.com`, `api.github.com`,
484
- `raw.githubusercontent.com`, `gist.githubusercontent.com`, `ghproxy.net`
485
- (mainland mirror for github raw), `stackoverflow.com`, `api.stackexchange.com`
486
- + `hn.algolia.com` (the two JSON search engines), `npmjs.org`, `pypi.org`,
487
- `learn.microsoft.com` — extend via
488
- `TM_WEBFETCH_ALLOWED_DOMAINS` (`"*"` opens every host; a custom list REPLACES
489
- the seed, so keep the engine hosts or `tm_search` loses its targets). Architect /
490
- implementer / reviewer have NO network grant — web questions come back as a
491
- reported gap, never simulated. The tester carries the host's native `browser_*`
492
- tools ONLY, for governed UI verification of the project (local dev servers,
493
- preview routes); open web fetching stays with the two network roles.
494
-
495
- **Out-of-allowlist targets are a gate, not a wall.** When a fetch / search /
496
- browser-open points at a host outside the allowlist, the tool hands the URL
497
- to OpenCode's **official confirmation dialog** — you decide, once per
498
- target (an unanswered dialog is auto-rejected on the usual 1-minute timer,
499
- and the plugin still never self-allows). Every dialog also fires a
500
- **system toast notification**, so you know something is waiting even when
501
- you're not staring at the screen. Env-file URLs and non-http(s) schemes
502
- remain hard-rejected with no dialog — R6 red lines are never consentable.
503
-
504
- ### Security: the R6 + R2 approval gate
505
-
506
- **On OpenCode 2.x, R6 is armed by default.** A Team role's native `read` of a `.env` is denied
507
- outright — no consent path — and a shell command that reads the environment goes to the host's own
508
- permission prompt (a plugin cannot raise a dialog on 2.x, so the shell face is the host's `ask`
509
- while the env-file face is a hard `deny`). Turn it off with the plugin option
510
- `envProtect: false` or with `TM_ENV_PROTECT=off`.
511
-
512
- **R6 environment protection.** With TeamMode active, the model cannot read
513
- environment variables silently. Env reads (`printenv`, `env`, `Get-ChildItem
514
- env:`, …) and env files (`.env`, shell rc) route through OpenCode's official
515
- confirmation dialog; unanswered prompts are **auto-rejected after
516
- `TM_ASK_TIMEOUT_MIN` (default 1 min)**. Env reads no wildcard can express
517
- (embedded `$VAR` / `${VAR}` / `$env:` inside another command, command
518
- substitution) and the `tm_*` wrapper channel stay a **hard block** — no dialog
519
- to slip through. The audit log records only tool name + pattern category +
520
- verdict — never command text, paths, variable names or values. Verdicts now
521
- also explain a dialog that cannot be answered anymore: a reply that hits an
522
- **already-closed** dialog (host 404 — the timer already auto-rejected) audits
523
- as `already-closed` without degrading the gate, a plugin-side bad reply shape
524
- audits as `rejected-shape-bug`, and a user reply that arrives *after* the
525
- auto-reject is recorded as `late-<verdict>` for observability only (the
526
- rejection stands — the plugin still never self-allows). The 1-minute auto-reject
527
- floor is tunable via `TM_ASK_TIMEOUT_FLOOR_MIN`; the short default is safe
528
- because a reply that races the timer audits as benign `already-closed` (host
529
- 404 on a closed dialog — no gate degrade), and the observed ~120 s is the
530
- host-to-plugin event-bus *delivery* lag, not click-resolution latency.
531
-
532
- **R2 dangerous operations (same dialog).** Delete, git publish, network
533
- fetch, package install/publish, process/system, privilege changes — none are
534
- silently allowed. The normal verification stack (`npm test`, `tsc`,
535
- `git status`) is NOT gated, so day-to-day work runs uninterrupted.
536
-
537
- > ⚠️ **When you approve a dialog, pick "once" — not "always".** Verified on
538
- > the live host, "always" records a far broader rule than the command you
539
- > saw: approving `Get-ChildItem env:PATH` with "always" stores `Get-ChildItem
540
- > *`, so every later `Get-ChildItem` runs with no dialog at all. The web
541
- > channel has the same trap and it is easier to fall into: one "always" on a
542
- > host lets **every agent session in that project** open it, so the per-agent
543
- > browser consent never gets asked again. The reply now says which path let a
544
- > page in (static allowlist / the dialog you just answered / a saved rule that
545
- > answered in milliseconds), because an agent that cannot tell them apart
546
- > reports "no dialog" as "that site is allowed".
547
-
548
- > Deferral to the dialog is per-session: env reads only route to the dialog
549
- > in sessions running TeamMode's injected agents. In any other session the
550
- > guard keeps hard-blocking env reads. `headless opencode run` auto-rejects
551
- > unanswered `ask`s immediately (there is no human to prompt).
552
-
553
- ### Repo hygiene
554
-
555
- The offload/trajectory/memory stores live under `<repo>/.git/opencode-team/`
556
- (outside a git repo: the OS temp dir, sharded per workspace as
557
- `opencode-team/w-<hash>/`, so one project never reads another's payloads) —
558
- **never your working tree**. Every
559
- agent is instructed to delete scratch files before reporting done and to keep
560
- throwaway work in the OS temp dir. A TTL sweeper reclaims old task dirs at
561
- startup + hourly; the Team Lead never deletes boards itself, so you can audit
562
- any run.
563
-
564
- ---
565
-
566
- ## ⚙️ Configuration
567
-
568
- The plugin injects everything at startup — no agent files to copy.
569
-
570
- > **Model choice matters.** Every judgment — triage, decomposition, dispatch
571
- > briefs, synthesis, review verdicts — flows through the Team Lead. A weak
572
- > model in that seat degrades the whole pipeline no matter how strong the
573
- > specialists are. Pin your best reasoning model to `team`:
574
-
575
- ```jsonc
576
- {
577
- "agent": {
578
- "team": { "model": "anthropic/claude-opus-4-5" }, // Lead earns your best model
579
- "implementer": { "model": "anthropic/claude-sonnet-4-6" } // specialists tolerate cheaper
580
- }
581
- }
582
- ```
583
-
584
- **Global install** — put the plugin entry in `~/.config/opencode/opencode.jsonc` and every project gets the team.
585
-
586
- **Keep Team off the default slot:**
587
-
588
- ```jsonc
589
- {
590
- "plugins": [
591
- { "package": "@te-river/opencode-team-mode@latest", "options": { "defaultAgent": false } }
592
- ]
593
- }
594
- ```
595
-
596
- Note the shape: on 2.x a plugin entry is either a string or an **object** with `package` and
597
- `options`. The 1.x tuple form `["@te-river/opencode-team-mode@latest", { … }]` is rejected
598
- (`path=$.plugins.1 kind=invalid`, measured 2026-10-06). Options do arrive — with the object
599
- form the boot row reads `board_ttl_days=7` for `{ "ttlDays": 7 }`, so this is measured, not
600
- documented-and-hoped.
601
-
602
- Your own agents named `team` / `architect` / … always take precedence; the
603
- plugin never clobbers user definitions. See [Customization](#-customization)
604
- for overrides, extra agents and disabling roles.
605
-
606
- ### Environment variables
607
-
608
- | Env var | Default | Purpose |
609
- |---|---|---|
610
- | `TM_ENV_PROTECT` | `strict` | R6 mode: `strict` / `standard` / `off` (off also disarms the approval timer) |
611
- | `TM_R6_FINE_ASK` | classifier | The command line is judged per-call by the host's `permission.evaluate` hook, so an ordinary `git status` asks nothing and an env dump still does. `off` falls back to asking about EVERY shell command — which is also what happens automatically on a host that does not expose the hook, and the boot note says which of the two caused it |
612
- | `TM_V2_CODEMODE` | catalog (opt-in `direct`) — **v2 only** | How our tools reach the model. OpenCode 2.x decides tool visibility with `options.codemode`. We used to send `codemode:false` by default and claim direct delivery — **a live 2.0.16 desktop session disproved it**: with the flag sent, every `tm_*` still arrived inside the host's Code Mode catalog ("They cannot be called directly…"), and the model's own callable list was the nine native tools. So the default now sends nothing, and `direct` is an experiment for a build that honours it. The observable truth is `tools_in_request` in the shutdown record (rendered by `tm_stats` as `请求内实际可见=…`), never the flag we sent |
613
- | `TM_LEDGER_MAX_ITEMS` | 200 | Ceiling on `tm_ledger` items per session. The host's `ctx.storage` has no TTL and no quota (measured), so the list refuses to grow past this instead of quietly dropping the oldest asks — and a refusal is something the lead can act on, while a silent truncation is a claim nobody can re-check |
614
- | `TM_V2_PROBE` | — (v2 only) | Path to a JSONL file where the surface probe records the host's real tool ids, permission action names and argument key names. Names and counts only — never a command line, path, URL or env value. It is how "does the host actually have X?" gets answered from the running build instead of from a doc; without it the same name sets still ride the trajectory so `tm_stats` can show them |
615
- | `TM_PRIVATE_SPACE` | `deny` | What private space (loopback, RFC1918, ULA, CGNAT, `.localhost`) does through our tools: `allow` opens the class, `deny` refuses it. The default is `deny` because a 2.x plugin cannot raise a dialog — telling the agent to wait for a window that will never open is not a gate with a procedure — so the refusal prints both operator exits instead (`allow`, or that one hostname in `TM_WEBFETCH_ALLOWED_DOMAINS`). Never conflated with the FORBIDDEN ranges: cloud-metadata / link-local / reserved are refused under every setting and every allowlist, with no consent path |
616
- | `TM_V2_BROWSER_GATE` | on — **v2 only** | The gate over the host's `browser_*` catalog (URLs and preview paths at `execute.before`, plus the browser URLs named inside an `execute` program). `off` restores the host's ungoverned browsing; the boot line and `tm_stats` say which world is running |
617
- | `TM_NATIVE_REPORT_MAX_TOKENS` | 1600 — **v2 only** | The budget a report-shaped native result (anything carrying a Markdown table — `tm_stats` via Code Mode, a `tm_join` summary) keeps in context. Table lines and their headings win the space and the prose between them is what gets dropped, because a table with a hole in it is not a table; the full text still rides the handle |
618
- | `TM_NATIVE_SNAPSHOT_MAX_TOKENS` | 1200 — **v2 only** | The budget a native `browser_snapshot` / `browser_find` keeps in context. Addressing lines win it before static text does; past `budget × 4` the reply states how many ref lines did not fit, and kept + dropped always equals the total |
619
- | `TM_ASK_TIMEOUT_MIN` | `1` | minutes before an unanswered dialog is auto-rejected (floored at 1 min — safe: a racing reply audits as benign `already-closed`; the host's reply event reaching the plugin ~120 s late is event-bus delivery lag, not a click delay). Every governed tool ALSO ends the wait itself at this + 15 s, because the gate is only armed when R6 is on and the client can reply — and a tool that waits forever reads as a hang, not as a request for your attention |
620
- | `TM_ASK_TIMEOUT_FLOOR_MIN` | `1` | minimum enforced for the ask timeout above |
621
- | `TM_ENV_PROTECT_EXTRA_DENY` | — | extra block patterns (regex; always hard block, never dialog-governed) |
622
- | `TM_OFFLOAD_THRESHOLD` | `2000` | global offload fallback (tokens, CJK-aware estimate) — used when the content class is unknown |
623
- | `TM_OFFLOAD_THRESHOLD_TEXT` | `4000` | offload boundary for prose (text / log / markdown) |
624
- | `TM_OFFLOAD_THRESHOLD_DATA` | `2000` | offload boundary for structured payloads (json / csv / code / binary) |
625
- | `TM_PREVIEW_MAX_TOKENS` | `80` | preview hard cap |
626
- | `TM_FETCH_MAX_LINES` | `2000` | tm_fetch page cap |
627
- | `TM_BLACKBOARD_DIR` / `TM_TRAJECTORY_DIR` | `<repo>/.git/opencode-team/…` | offload store / trajectory ledger (tmpdir fallback, sharded per workspace by a path hash so `tm_stats's window is only THIS workspace's traffic; explicit = absolute or project-relative) |
628
- | `TM_BLACKBOARD_TTL` | `7` | store retention (days) |
629
- | `TM_BOARD_MAX_CHARS` | `200000` | tm_board_write: one board file's character cap — over it the write refuses (with a "split the topic" hint) instead of truncating a deliverable |
630
- | `TM_BOARD_MAX_FILES` | `200` | tm_board_write: markdown files allowed per session folder; the TTL sweeper is the only reclaim path, so the refusal names it and the `ttlDays` option |
631
- | `TM_SEARCH_DEFAULT_ENGINE` | `auto` | tm_search engine when no `engine` arg is given (`auto` = classify + parallel fan-out + RRF fusion; any table name also pins a manual default) |
632
- | `TM_WEBFETCH_ALLOWED_DOMAINS` | `"*"` | tm_webfetch / tm_search allowlist (`"*"` opens all; empty = deny all; a custom list REPLACES the seed — keep the engine hosts). `"*"` does **not** cover private space: loopback / RFC1918 / CGNAT / `.localhost` still need the dialog on every use, and non-routable ranges (169.254.0.0/16 metadata, 0.0.0.0/8, multicast, reserved, plus the IPv4-mapped and DNS64 spellings of the same target) are a hard red line no setting can open |
633
- | `TM_WEB_CACHE_TTL_SEC` | `300` | how long a governed fetch may re-serve the same URL (0 = off). One store shared by tm_webfetch / tm_search; entries are hash-named (a token-bearing query never hits disk) and are only read or written for hops the STATIC allowlist admitted — dialog consent stays per-request, and a re-served page says 缓存命中 |
634
- | `TM_SEARCH_WEIGHTS` | unset | per-engine fusion weight overrides, e.g. `bing=0.3,hn=0.25`; anything unset keeps the built-in table |
635
- | `TM_SEARCH_RELEVANCE_FLOOR` | `0.35` | weight fraction kept by a hit that shares no query token with its title/snippet/host (demotes junk without deleting an engine) |
636
- | `TM_SEARCH_MAX_HITS` | `10` | hits kept per engine leg and in the fused list |
637
- | `TM_SEARCH_DISABLED_ENGINES` | unset | engines removed from the roster AND from every `auto` route (`sogou,baidu` style) |
638
- | `TM_BASH_TIMEOUT_PROBE_MS` | `60000` | ceiling forced onto a `timeout` the model set for a read-only probe command (0 disables) |
639
- | `TM_PRUNE` | `on` | `off` stops context pruning entirely (settled message bodies stay in the request) |
640
- | `TM_PRUNE_AT_PERCENT` | `70` | prune once the request reaches this share of the model's window; derived from `limit.context`, never a fixed token count. **Clamped to 40–95** — a lower value is silently raised to 40, so a reproduction that sets 5 is really testing 40 |
641
- | `TM_PRUNE_KEEP_TAIL_PERCENT` | `40` | share of the window kept verbatim at the tail; the newest message is always kept |
642
- | `TM_RETRY` | `on` | `off` stops the retry governor (no backoff directive, no cooldown) |
643
- | `TM_RETRY_BASE_MS` | `5000` | first backoff after a quota/rate-limit error; doubles per consecutive error |
644
- | `TM_RETRY_MAX_MS` | `60000` | ceiling for that backoff |
645
- | `TM_RETRY_JITTER` | `0.3` | ± fraction applied to the wait, so parallel sessions do not re-fire together |
646
- | `TM_RETRY_BREAK_AFTER` | `5` | consecutive errors before the cooldown starts |
647
- | `TM_RETRY_COOLDOWN_MS` | `60000` | how long new sub-agent dispatches are refused once the breaker trips |
648
- | `TM_SPLIT_ADVICE` | `on` | `off` stops the split hint (the prompt discipline stays) |
649
- | `TM_SPLIT_BRIEF_TOKENS` | `4000` | a dispatch brief above this size earns a split hint on the next request. **Clamped to ≥200** — a lower value is silently raised, so a reproduction that sets 50 is really testing 200 |
650
- | `TM_SPLIT_MAX_CRITERIA` | `3` | the prompt's own rule: more acceptance criteria than this is a signal to split |
651
- | `TM_BASH_TIMEOUT_MAX_MS` | `0` | optional global ceiling for every other bash command — off by default so a real build keeps the timeout it asked for |
652
- | `TM_JOIN_MAX_WAIT_MS` | `60000` | ceiling on `tm_join { waitMs }`. Was 300 000, and a lead parked in it twice in a row (19 min of nothing) while its children worked — waiting is not parallelism, so the default now says "check, then work". A second consecutive wait after nothing settled is cut to 10 s and answered with what to do instead |
653
- | `TM_STORE_RECLAIM` | `on` | at boot, reclaim what an upgrade left behind: a per-workspace store shard idle past the TTL, and the pre-shard `blackboard/` + `trajectory/` under the temp-dir fallback (503 MB of expired runs on one real machine, with no sweeper pointed at them any more). Only TTL-expired entries are ever removed — a fresh run dir survives, because a session started before the upgrade may still be writing there. `off` leaves the disk exactly as found (the test runner sets it) |
654
- | `TM_TASK_OFFLOAD` | `on` | keep the HOST's background sub-agent inside the context budget: when `task { background: true }` finishes, the host injects the child's full reply into your session; this replaces an oversized body with a preview + a `tm_fetch`-style pointer (`tm_join { ids: [...] }`), touching ONLY synthetic parts that carry the host's own `<task id=… state="completed">` envelope and exceed the text offload threshold. Nothing is copied to disk — the text stays where it was written (the child session). `off` restores the host's verbatim injection. **On OpenCode 2.x this half does not apply**: a v2 plugin is never handed the injected message before it is persisted, and we deliberately do NOT rewrite outgoing messages (a wrong guess about that layer's shape deletes evidence silently — the v1 `experimental.chat.messages.transform` path is left unimplemented on purpose). The compensation there is contractual, not a rewrite: the child's oversized deliverable goes to the blackboard file and its reply carries the path, so the lead reads the summary and pulls the whole thing with `tm_join` |
655
- | `TM_TOOL_HINTS` | `on` | append TeamMode's call-site discipline to the built-in `bash` / `task` tool DESCRIPTIONS via `tool.definition` (append-only, idempotent — the host text is never replaced) |
656
- | `TM_AGENT_TEMPERATURE` | `off` | `on` applies a per-role sampling table (architect 0.35 / researcher 0.3 / reviewer 0.1 / rest 0.2) via `chat.params`; or give it `reviewer=0.05;team=0.4`. Off = the documented "all agents at 0.2" invariant stands |
657
- | `TM_COMPACTION_CONTEXT` | `on` | on the host's pre-compaction hook, add the must-survive list (reply skeleton, offload handles, open sub-agent session ids, provenance, board paths). Additive — the host's own summarizer prompt is never replaced |
658
- | `TM_COMPACTION_AUTOCONTINUE` | `on` | `off` stops the host from silently resuming the turn after a compaction, so a human re-reads state first |
659
- | `TM_SHELL_NO_COLOR` | `on` | inject `NO_COLOR`/`TERM=dumb` into every child shell via `shell.env` (ANSI progress bars are pure context tax). Never overwrites a value the host already set |
660
- | `TM_SHELL_ENV` | — | explicit `KEY=VALUE;KEY2=VALUE2` passthrough into child shells — deliberately allowlisted, so this hook can't become a side channel for the parent environment |
661
- | `TM_MEMORY_GLOBAL_DIR` | `~/.opencode-team/memories/global/` | tm_memory GLOBAL tier store |
662
- | `TM_MEMORY_SESSION_TTL_MIN` | `240` | session-tier entry TTL (lazy + boot sweep) |
663
- | `TM_MEMORY_MAX_ENTRIES` | `200` | per-scope entry cap; over it `add` fails on purpose — run `compact` |
664
- | `TM_MEMORY_STALE_DAYS` | `30` | age after which search hits are tagged `[stale Nd]` (`0` disables) |
665
- | `TM_MEMORY_SESSION_PERSIST` | — (ephemeral) | `1` also writes session entries under `memories/sessions/<sid>/` |
666
- | `TM_HIT_BLACKLIST` | `maimai.cn` | extra domains never listed as search hits (comma/semicolon separated; same-name-different-site noise like 脉脉) |
667
-
668
- ---
669
-
670
- ## 🏗️ How the team works
671
-
672
- Sub-agents can't message each other live (platform limitation), so TeamMode
673
- coordinates them through a **structured reply skeleton** — every specialist
674
- reply is `STATUS: / CHANGES: / FINDINGS: / EVIDENCE: / HANDOFF:`, ≤50 lines,
675
- relayed verbatim by the Lead into the next dispatch. Files are the exception,
676
- not the rule: a deliverable over ~50 lines goes to ONE named board file under
677
- `<repo>/.git/opencode-team/` (round-suffixed, working tree untouched).
678
-
679
- - **Routing table:** question → direct answer; docs-only → implementer;
680
- product change → implementer → tester → reviewer; multi-module →
681
- architect → implementer → tester → reviewer(s); unknown tech → researcher
682
- first. Fixed minimums — a product change routed below 3 dispatches is a
683
- routing bug.
684
- - **Approval gate (count-based):** ≥2 dispatches → plan (≤30 lines) → **your
685
- approval** → execute. Blocking questions are batched and asked immediately.
686
- - **Concurrent dispatch:** independent dispatches batch into the SAME round
687
- (parallel implementers with per-file ownership + verbatim contracts,
688
- 3-dimension reviews, split test suites); only the Team Lead dispatches —
689
- specialists no longer hold `task`, so no sub-agent spawns a sub-agent.
690
- - **Adaptive review:** one reviewer by default; three parallel dimensions
691
- only for high-risk profiles (auth/security, cross-module contracts, public APIs).
692
- - **Static verification:** build / typecheck / lint / tests. Improvised
693
- browser automation is banned; unverified UI work ends with
694
- `UI NOT VERIFIED: <what to check>`.
695
- - **Evidence standard:** "done / fixed / passed" claims need verifiable
696
- evidence — output, logs, diffs.
697
-
698
- ---
699
-
700
- ## 🔧 Customization
701
-
702
- **Override an agent** — same name in your config wins:
703
-
704
- ```jsonc
705
- {
706
- "agent": {
707
- "reviewer": {
708
- "model": "anthropic/claude-sonnet-4-6",
709
- "prompt": "You are an extremely strict reviewer. Reject anything with a lint warning."
710
- }
711
- }
712
- }
713
- ```
714
-
715
- **Add your own agents** alongside the team:
716
-
717
- ```jsonc
718
- {
719
- "agent": {
720
- "devops": {
721
- "mode": "subagent",
722
- "description": "Handles CI/CD, Docker, and deployment tasks.",
723
- "prompt": "You are the DevOps engineer..."
724
- }
725
- }
726
- }
727
- ```
728
-
729
- **Disable one:** `"researcher": { "disable": true }`.
730
-
731
- **Board retention** via the plugin entry's `options`:
732
-
733
- ```jsonc
734
- {
735
- "plugins": [
736
- { "package": "@te-river/opencode-team-mode@latest", "options": { "ttlDays": 7 } }
737
- ]
738
- }
739
- ```
740
-
741
- Valid range (0, 365], invalid values fall back to 5. Measured 2026-10-06: with this shape the
742
- boot row reads `board_ttl_days=9` for `ttlDays: 9`. (The 1.x tuple form is rejected on 2.x:
743
- `path=$.plugins.1 kind=invalid`.)
744
-
745
- ---
746
-
747
- ## ❓ FAQ
748
-
749
- **Will this eat my tokens?**
750
- The opposite is the point. Offload + 80-token previews + the host's Code Mode
751
- `execute` exist because a five-agent pipeline naively bolted onto one context window
752
- *would* eat your tokens. The governance is the token-saver.
753
-
754
- **Is the web access safe?**
755
- It's the most guarded surface in the plugin: two full web roles plus a
756
- browser-only tester grant, a domain
757
- allowlist with dialog-gated escapes (you approve any out-of-allowlist
758
- target in OpenCode's official dialog, with a toast notification), redirects
759
- re-checked per hop, network-layer enforcement in the browser, env-file URL
760
- refusal, and every payload rides the same offload governance. No allowlisted
761
- page can bounce the fetch off-site.
762
-
763
- **Why doesn't the plugin auto-update?**
764
- OpenCode caches plugins by spec string and never re-resolves `@latest`
765
- (upstream limitation, not ours). **Re-run the installer — that IS the
766
- update** (it purges the cache and re-resolves npm copies); or delete the
767
- cache dir by hand. Recipe above and in the
768
- [installation guide](./docs/installation.md).
769
-
770
- **Can agents run tools in parallel?**
771
- Yes — and they're *engineered* for it: parallel `tm_search` / `tm_webfetch` /
772
- `tm_fetch` calls get distinct step ids and isolated payloads. A regression
773
- here fails the test suite before it ever reaches you.
774
-
775
- **Does it work in the CLI (TUI), or only Desktop?**
776
- Both. Desktop adds the color-coded picker and panels; the governed tools and
777
- the whole workflow are host-agnostic. Interactive browsing is the host's own
778
- `browser_*` catalog, so a host with no native browser (CLI / standalone) has
779
- none — the agent reports that gap rather than simulating one.
780
-
781
- **What happens if I don't answer a confirmation dialog?**
782
- It auto-rejects after `TM_ASK_TIMEOUT_MIN` (default 1). The plugin never
783
- self-approves — the only side it can take is yours or nobody's.
784
-
785
- ---
786
-
787
- ## 🗑️ Uninstall
788
-
789
- 1. Remove the entry from the `"plugins"` array in your config file; if you installed on 2.x, delete the generated `~/.config/opencode/agents/*.md` and `commands/team-*.md` too, and any `default_agent: "Team"` you no longer want.
790
- 2. Delete the cache dir (table in [Install](#-read-this-once-save-yourself-an-hour-later)) if you want the disk space back.
791
- 3. Restart OpenCode. The agents, commands and tools are gone; the stores under `<repo>/.git/opencode-team/` (and `~/.opencode-team/` for global memories) are plain files you can delete whenever.
792
-
793
- No DLLs were harmed. Nothing was written to your working tree.
794
-
795
- ---
796
-
797
- ## 🏛️ Architecture (for the curious)
798
-
799
- ```
800
- opencode-team-mode/
801
- ├── src/
802
- │ ├── index.ts ← Plugin entry ({id, setup} — v2 only)
803
- │ ├── agents.ts ← Agent structure (modes, colors, temperatures, whitelist matrix)
804
- │ ├── prompts/ ← Agent prompts (lead / specialists / shared) — pinned by tests
805
- │ ├── commands.ts ← Slash command definitions
806
- │ ├── blackboard.ts ← Shared blackboard + TTL sweeper
807
- │ ├── envprotect.ts ← R6 facade (patterns / classifiers / gate predicates / hook)
808
- │ ├── identity.ts ← Agent-name identity (case-insensitive lead match)
809
- │ ├── tm/ ← Governed tools: pipelines / store / preview / guard / refs /
810
- │ │ webfetch / search / memory / browser / shell-bridge / board / ledger
811
- │ ├── host/ ← v2 personality (setup / guard / offload / session / events / …)
812
- │ └── types.ts ← Loader-contract types
813
- ├── docs/installation.md ← Historical 1.18.x guide (no longer supported)
814
- ├── docs/installation-v2.md ← The 2.x install guide (roles arrive as generated config files)
815
- ├── scripts/ ← One-line installers (bash / PowerShell)
816
- ├── pt07/ ← PT-07 baseline suite (seeded A/B token measurement)
817
- └── README.*.md ← You are here (twice)
818
- ```
819
-
820
- The loader calls `setup(ctx)` once: it registers the `tm_*` tools, installs the
821
- R6/address guard on `permission.evaluate`, attaches the JIT offload on
822
- `tool.execute.after`, and publishes the blackboard note. User-defined agents with
823
- the same name always win — the plugin never clobbers.
824
-
825
- ---
826
-
827
- ## 🤝 Contributing
828
-
829
- Issues and PRs welcome. Especially wanted: localization of agent prompts,
830
- more agent roles, more command templates, and real-world reports of the
831
- search engines' behavior (they rearrange their markup; the extractor filters
832
- are intentionally loose but not psychic).
833
-
834
- ---
835
-
836
- ## 📄 License
837
-
838
- [Apache License 2.0](./LICENSE)
839
-
840
- ---
841
-
842
- ## 🔗 Links
843
-
844
- - [npm Package](https://www.npmjs.com/package/@te-river/opencode-team-mode) — `@te-river/opencode-team-mode`
845
- - [Installation guide, **OpenCode 2.x**](./docs/installation-v2.md) — the supported install: a 2.x plugin cannot create an agent, so the six roles and six commands are generated into the config directory
846
- - [Installation guide, 1.18.x](./docs/installation.md) — **historical only**: the 1.18.x personality was removed in the 1.7.0 line, so this page is kept for reference and is not a working install path
847
- - [OpenCode Desktop](https://opencode.ai) — Official website & download
848
- - [OpenCode Docs](https://opencode.ai/docs) — Configuration & plugin documentation
849
- - [OpenCode Plugin API](https://opencode.ai/docs/plugins) — Build your own plugins
1
+ # OpenCode TeamMode
2
+
3
+ **[English](./README.md)** | **[中文](./README.zh-CN.md)**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@te-river/opencode-team-mode.svg)](https://www.npmjs.com/package/@te-river/opencode-team-mode)
6
+ [![npm downloads](https://img.shields.io/npm/dm/@te-river/opencode-team-mode.svg)](https://www.npmjs.com/package/@te-river/opencode-team-mode)
7
+ [![license](https://img.shields.io/npm/l/@te-river/opencode-team-mode.svg)](./LICENSE)
8
+
9
+ > 🤝 **Your OpenCode just hired a team.**
10
+ >
11
+ > Six specialized agents — a Lead, an Architect, an Implementer, a Reviewer, a Tester and a Researcher — with governed tools, structured handoffs, and a plan-first approval gate. One plugin, no files to copy.
12
+
13
+ > 💡 **Best for medium-to-large projects.** The governance layer (approval gate, context offload, tool allowlists) is an asset once a codebase has real surface area — and mostly overhead on tiny scripts and one-off questions. Use it where the work is.
14
+
15
+ ---
16
+
17
+ ## TL;DR — skip the docs
18
+
19
+ > **The lazy path:** paste this to any coding agent and let it do the work:
20
+ >
21
+ > ```text
22
+ > Install the OpenCode plugin @te-river/opencode-team-mode. This package supports **OpenCode 2.x
23
+ > only** — the 1.18.x personality was removed in the 1.7.0 line:
24
+ > 2.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation-v2.md
25
+ > (a 2.x plugin cannot create an agent, so the six roles and six `/team-*` commands come
26
+ > from a generator step in that guide — that step is not optional.) Then verify the install
27
+ > using the checks in that guide.
28
+ > ```
29
+ >
30
+ > **The one command to remember afterwards:** `/team-run <task>` — the Team Lead
31
+ > plans it, shows you the plan, waits for your approval, then runs the whole team.
32
+
33
+ Everything else below is detail. When you're ready for it, here's the map:
34
+
35
+ **[Why](#-why-teammode) · [The team](#-the-team) · [Install](#-install) · [Usage](#-usage) · [Tools & security](#-governed-tools--security) · [Configuration](#️-configuration) · [How it works](#-how-the-team-works) · [Troubleshooting](#-troubleshooting) · [Uninstall](#-uninstall)**
36
+
37
+ ---
38
+
39
+ ## 🤔 Why TeamMode?
40
+
41
+ Because a single agent doing everything is how you get: a context window stuffed
42
+ with 5000-line file dumps, twenty round-trips to `shell` for what one script could
43
+ do, sub-agents that silently read `.env`, and web "research" that is really just
44
+ the model's imagination.
45
+
46
+ TeamMode's answer to each:
47
+
48
+ | Pain | TeamMode's answer |
49
+ |---|---|
50
+ | 🔥 **Context flooding** | Every governed result over its content-class offload boundary (prose 4000 / data 2000 tokens, CJK-aware) is offloaded to a local run store and replaced by an 80-token preview + an HMAC handle. The agent pages through what it needs — the window never drowns. |
51
+ | 🐌 **Round-trip overhead** | The host's own `execute` (Code Mode): the agent writes ONE program that makes N governed calls in a single turn. Zero LLM round-trips during the run. |
52
+ | 🕳️ **Silent side effects** | The R6 env-protection face: a Team role's `read` of a `.env` is **denied** with no consent path, and a shell command that reads the environment goes to the host's own permission prompt. The plugin never approves on its own — it can only reject. |
53
+ | 🌫️ **Hallucinated research** | Web access is a two-role grant with an allowlisted, governed tool chain. A fact that couldn't be fetched is reported as a gap — never fabricated. |
54
+ | 🧭 **Walls of text** | Replies are steered into the shape the host renders fastest: a markdown table for per-file / per-case / per-finding results, fenced code for diffs and configs, a browser screenshot attached as an inline image only when you ask for one. The renderer's supported set is measured rather than assumed, and the prompts carry the negative half of that measurement — footnotes, `==highlight==`, a bare `---` rule and `$…$` math reach you as literal text, so agents are told not to use them, while `mermaid` diagrams are offered because this host does draw them. |
55
+ | 🎯 **Goal drift** | The lead opens with `GOAL:` in your own words plus checkable `ACCEPTANCE:` criteria, and the run does not end while a criterion lacks evidence — the only legitimate stops are named (blocked on you, or provably unachievable). When a round settles with items still open on the lead's ledger, `tm_join` says 目标未达成 and lists them, and the goal is carried through context compaction so a summarized transcript cannot redefine it. |
56
+ | 🗣️ **Replies in a language you never chose** | The governed tools answer in Chinese, and an agent left to its own devices mirrors that straight back at you. Every role carries a reply-language rule: your own language wins, and a Chinese string is quoted verbatim only where it IS the evidence (a close verdict, a refusal line) — translating a verdict is how an unchecked claim starts looking checked. |
57
+ | ⏱️ **Rounds spent for the sake of looking careful** | 效率至上 is written into the lead and all five specialists: one wide call instead of three narrow ones, independent calls in the same round, ≥3 probes collapsed into one `execute` (Code Mode) program, no re-running a check to watch it pass again — with the boundary stated too: efficiency never buys its way out of the evidence rule, because an unverified "done" costs you the round *and* the bug. |
58
+
59
+ And the workflow discipline underneath: deterministic routing, a ≤30-line plan
60
+ you approve before ≥2 dispatches execute, structured `STATUS/CHANGES/FINDINGS/
61
+ EVIDENCE/HANDOFF` replies between agents, and static verification (build /
62
+ typecheck / tests) instead of vibes.
63
+
64
+ That discipline is also why the recommendation is **medium-to-large
65
+ projects**: on a two-file script the team simply has less to govern.
66
+
67
+ ---
68
+
69
+ ## 👥 The team
70
+
71
+ | Agent | Role | When to use |
72
+ |---|---|---|
73
+ | 🎯 **Team Lead** (`@team`) | Orchestrator | Complex tasks that need planning + multi-step execution |
74
+ | 🏗️ **Architect** | System designer | Design docs, module structure, API contracts — **dispatched only when the design is genuinely unknown** |
75
+ | 💻 **Implementer** | Code writer | Building features, writing production code |
76
+ | 🔍 **Reviewer** | Dimension-focused auditor | Single-dimension review by default; 3 in parallel only for high-risk changes |
77
+ | 🧪 **Tester** | Test engineer | Tests with real edge cases; static verification (build / typecheck / lint); governed UI verification via the host's native `browser_*` tools |
78
+ | 🔎 **Researcher** | Knowledge finder | Local repo first, then the web — one of the two network roles (with the Lead) |
79
+
80
+ Out of the box, **Team is your default agent** — new chats open straight into the
81
+ orchestrator (opt-out in [Configuration](#️-configuration)).
82
+
83
+ ---
84
+
85
+ ## 📦 Install
86
+
87
+ ### Option 1 — Let your agent do it (recommended)
88
+
89
+ Copy this into any coding agent — it will edit your config, restart-remind you, and verify:
90
+
91
+ ```text
92
+ Install the OpenCode plugin @te-river/opencode-team-mode. This package supports **OpenCode 2.x
93
+ only** — the 1.18.x personality was removed in the 1.7.0 line:
94
+ 2.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation-v2.md
95
+ (a 2.x plugin cannot create an agent, so the six roles and six `/team-*` commands come from
96
+ a generator step in that guide — that step is not optional.)
97
+ (If a URL is unreachable — common on mainland-China networks — retry with
98
+ the mirror prefix: https://ghproxy.net/ + the same path.)
99
+ Then verify the install using the checks in that guide.
100
+ ```
101
+
102
+ (The guide is the complete manual procedure — config file locations, plugin
103
+ entry, restart, verification, update and uninstall. Your agent reads it and
104
+ executes it faithfully; there is nothing else it needs.)
105
+
106
+ ### Option 2 — One-line script
107
+
108
+ macOS / Linux (bash):
109
+
110
+ ```bash
111
+ curl -fsSL https://ghproxy.net/https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/scripts/install.sh | bash
112
+ ```
113
+
114
+ Windows (PowerShell):
115
+
116
+ ```powershell
117
+ irm https://ghproxy.net/https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/scripts/install.ps1 | iex
118
+ ```
119
+
120
+ ### Option 3 — Manual
121
+
122
+ Add the plugin to your `opencode.jsonc`:
123
+
124
+ ```jsonc
125
+ {
126
+ "$schema": "https://opencode.ai/config.json",
127
+ "plugins": [
128
+ "@te-river/opencode-team-mode@latest"
129
+ ]
130
+ }
131
+ ```
132
+
133
+ OpenCode installs the plugin on next startup.
134
+
135
+ > **The key is `plugins` (plural).** A 2.x host never reads the singular `plugin`
136
+ > key, and it installs the package itself from the entry at startup. Entries
137
+ > across config files are **applied lowest→highest rather than replacing one
138
+ > another**, so keep exactly one Team entry — writing ours into both
139
+ > `opencode.json` and `opencode.jsonc` loads the plugin twice. See
140
+ > [docs/installation-v2.md](./docs/installation-v2.md) for the 2.x flow (a plugin
141
+ > cannot create an agent there, so the six roles and six commands are generated
142
+ > config files);
143
+ > `docs/research/plugin-loader-contract.md` has the host's own loader code.
144
+
145
+ ### ⚠️ Read this once, save yourself an hour later
146
+
147
+ - **Restart to activate.** After touching the OpenCode config, fully quit and restart OpenCode (Desktop: quit from tray, not just the window).
148
+ - **Plugin updates: re-run the installer.** It is idempotent — a re-run re-patches the config (no-op when present), purges the stale plugin cache, and re-resolves any npm-installed copy. This exists because OpenCode caches plugins by spec string and does NOT re-resolve `@latest` when a new version publishes (upstream limitation). Manual recipe, if you prefer:
149
+
150
+ | OS | Purge command — recursive, covers BOTH cache layouts |
151
+ |---|---|
152
+ | macOS / Linux | `find ~/.cache/opencode/packages -type d -name '*opencode-team-mode*' -prune -exec rm -rf {} +` |
153
+ | Windows (PowerShell) | `foreach ($r in "$HOME\.cache\opencode\packages", "$env:LOCALAPPDATA\opencode\cache\packages") { if (Test-Path $r) { Get-ChildItem $r -Directory -Recurse -Filter '*opencode-team-mode*' -EA SilentlyContinue | Sort-Object { $_.FullName.Length } | Remove-Item -Recurse -Force -EA SilentlyContinue } }` |
154
+
155
+ ⚠️ OpenCode loads the plugin from `~/.cache/opencode/packages/` — possibly nested inside the scoped `@te-river/` directory — **not** from `~/.config/opencode/node_modules`, which is why the delete must recurse (a top-level-only `rm -rf` silently misses the scoped copy).
156
+
157
+ ⚠️ **A published version can be shadowed by your HOME.** For a package spec the host resolves the entrypoint through the nearest `node_modules` walk starting at HOME, so a stray `~/package.json` with a `devDependencies` entry pinning `@te-river/opencode-team-mode` makes the host load *that* copy. The diagnostic is the boot probe's `entrypoint=` line, not a guess about cache layers. Full recipe (including an agent-driven update prompt): [installation guide, Updating](./docs/installation-v2.md).
158
+ - **Prerequisites:** [OpenCode](https://opencode.ai) (Desktop or CLI) and Node ≥ 18.
159
+
160
+ ### 🖥️ What the installer changes on your machine
161
+
162
+ Three things, all outside this repo — listed because a plugin that edits your
163
+ environment without saying so is not one you can trust.
164
+
165
+ | What | Where it lands | How long it stays |
166
+ |---|---|---|
167
+ | A plugin entry in the OpenCode config | `~/.config/opencode/opencode.jsonc` (created if missing; an existing `opencode.json` is **migrated into** the `.jsonc`, the original file left untouched) | until you delete the line |
168
+ | A purged plugin cache | `~/.cache/opencode/packages/*opencode-team-mode*` (including the nested `node_modules` copy that actually runs) | re-downloaded on next start — it is a cache, nothing to restore |
169
+ | **A user-level environment variable** | `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` | **Windows: `setx` writes it into your user profile — it survives reboot and is visible to EVERY program you start afterwards, not just OpenCode.** macOS `launchctl setenv` / Linux `systemctl --user set-environment`: this login session only (re-run the installer after a reboot) |
170
+
171
+ Why that third one is there: `task { background: true }` is the only sub-agent
172
+ the OpenCode interface can **show** you — a card linking to the live child
173
+ session, which you can stop. The host turns it on through a flag on **its own
174
+ process**, and a plugin cannot set flags in the process that loads it, so the
175
+ flag has to come from the environment. Without it nothing breaks: `task` simply
176
+ blocks the lead and its card stays non-background. (On 2.x the plugin forces
177
+ `background: true` on every `subagent` call regardless — this flag is about the
178
+ *host's* `task` tool, which is the channel a human can watch.)
179
+
180
+ Undo it:
181
+
182
+ ```powershell
183
+ # Windows — remove it from your user profile
184
+ [Environment]::SetEnvironmentVariable("OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS", $null, "User")
185
+ ```
186
+
187
+ ```bash
188
+ launchctl unsetenv OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS # macOS
189
+ systemctl --user unset-environment OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS # Linux
190
+ ```
191
+
192
+ Or skip it at install time, and the installer touches only the two files above:
193
+
194
+ ```powershell
195
+ .\install.ps1 -NoBackgroundSubagents # or $env:TEAMMODE_SKIP_BACKGROUND_SUBAGENTS="1"
196
+ ```
197
+
198
+ ```bash
199
+ TEAMMODE_SKIP_BACKGROUND_SUBAGENTS=1 bash install.sh
200
+ ```
201
+
202
+ Either way the installer prints what it changed, and reads the value back
203
+ instead of trusting its own exit code.
204
+
205
+ ### Verify
206
+
207
+ Restart, open the agent picker, and look for **team, architect, implementer,
208
+ reviewer, tester, researcher**. That's it — the team is hired.
209
+
210
+ ---
211
+
212
+ ## 📖 Usage
213
+
214
+ ### Slash commands
215
+
216
+ | Command | Agent | Description |
217
+ |---|---|---|
218
+ | `/team-plan <task>` | Architect | Implementation plan with architecture, file manifest, task breakdown |
219
+ | `/team-implement <task>` | Implementer | Production code for a feature or task |
220
+ | `/team-review [scope]` | Reviewer | Audit for bugs, security issues, quality problems |
221
+ | `/team-test [scope]` | Tester | Comprehensive tests with edge-case coverage |
222
+ | `/team-research <topic>` | Researcher | Local repo first; web via the governed tools |
223
+ | `/team-run <task>` | Team Lead | **The full workflow** — plan → approval → orchestration → verification |
224
+
225
+ Or `@`-mention agents directly: `@team`, `@architect`, `@implementer`,
226
+ `@reviewer`, `@tester`, `@researcher`.
227
+
228
+ ### What a run feels like
229
+
230
+ Team is the default agent out of the box, so you just type into a fresh chat:
231
+
232
+ > **You:** Add token-bucket rate limiting to our Express API — 100 requests per minute per user, return 429 when exceeded. Don't touch anything under `src/legacy/`.
233
+
234
+ ```text
235
+ team Route: product behavior change → implementer → tester → reviewer
236
+ (3 dispatches → approval gate applies)
237
+
238
+ PLAN
239
+ Goal: token-bucket limiter, 100 req/min/user, 429 + Retry-After
240
+ Scope: src/middleware/rateLimit.ts (new), src/app.ts (+3 lines)
241
+ Pipeline: implementer → tester → reviewer (correctness)
242
+ Assumption: in-memory bucket, not Redis — say the word to change
243
+ Boundary: src/legacy/** untouched
244
+ Approve to proceed?
245
+
246
+ you approved, go
247
+
248
+ team ▸ @implementer — STATUS: done
249
+ CHANGES: src/middleware/rateLimit.ts (new) · src/app.ts (+3)
250
+ EVIDENCE: tsc clean
251
+
252
+ ▸ @tester — STATUS: done · VERDICT: pass (14/14)
253
+ FINDINGS: burst edge, window expiry, concurrent refill covered;
254
+ src/legacy/** byte-identical
255
+
256
+ ▸ @reviewer — STATUS: done · VERDICT: approve (correctness)
257
+
258
+ team Done. rateLimit.ts (new) · app.ts (+3) · 14 tests green
259
+ review: approve · assumption: in-memory only · src/legacy/ untouched
260
+ ```
261
+
262
+ You typed the task and four words. The plan was a routing-table lookup,
263
+ execution waited for your approval, and every handoff between agents traveled
264
+ as a structured skeleton — nothing landed in your repo, nothing was guessed.
265
+
266
+ ---
267
+
268
+ ## 🧰 Governed tools & security
269
+
270
+ Every tool TeamMode adds runs under ONE governance pipeline: outputs above
271
+ the offload threshold never enter the context window — the boundary is
272
+ content-class aware (`offloadThresholdText` = 4000 for prose: text/log/markdown;
273
+ `offloadThresholdData` = 2000 for json/csv/code/binary; an unknown class falls
274
+ back to the global `offloadThreshold`) — and oversized results are offloaded to a
275
+ run store and replaced by a content-aware preview plus an HMAC-signed handle that
276
+ the agent pages through with `tm_fetch` when it genuinely needs the payload.
277
+
278
+ | Tool | What it does | Roles |
279
+ |---|---|---|
280
+ | `tm_fetch` | Paged handle retrieval for offloaded results (JSON handles take a `fields` dot-path projection — a deliberately small jq subset like `items[].name`) | all six agents |
281
+ | `tm_memory` | Session + project + global memory store (Markdown + frontmatter): add / search / list / forget / compact | all six agents |
282
+ | `tm_board_write` | **The blackboard's write side**: places ONE new Markdown file at `<board-root>/<session-key>/<task-slug>/NN-<role>-<topic>[-rN].md` and chooses the name itself — a revision is a new round-suffixed file, never an overwrite, and the reply carries the path plus the byte count, never the content. It exists because the board used to need a file tool, and `architect` / `researcher` own none (no `write`, no `edit`, no `shell` even to stamp the session folder), so every oversized deliverable from those roles came back as `BLACKBOARD WRITE FAILED` plus the whole document pasted inline — the reply shape this team mandates was un-followable exactly where it mattered. Scope is enforced rather than asked: segments sanitized, target realpath-verified against the board root (a symlinked task dir is refused), the name always ends in `.md` so no `.env`/rc file can be produced, capped by `boardMaxChars` + a per-session file limit | All six agents |
283
+ | `tm_search` | Multi-engine web search with extracted, deduplicated, RRF-fused hit lists | Lead + Researcher |
284
+ | `tm_webfetch` | Single governed GET of an allowlisted page (search pages auto-extracted). Manual redirects, re-checked per hop, and a refusal names the WHOLE chain (`跳转链: a → b(停在第 2 跳)`) — an allowlisted shortener that bounces off-site used to report only the off-site host, which read as "that site will not fetch" and sent the agent back to retry the entry URL it had just watched fail. A 429/503 carries its own `Retry-After` when the server sends delta-seconds (an HTTP-date is deliberately not laundered into a countdown), so "come back later" never looks like "no content here". The page GET also asks for Markdown first (`Accept: text/markdown,…`) — measured on `learn.microsoft.com`: 60,778 B of HTML becomes 11,449 B of Markdown, and every other host tried returns the same document either way, so the preference is free where it is ignored | Lead + Researcher |
285
+ | `tm_ledger` | **The lead's task ledger** (`add` / `doing` / `done` / `blocked` / `list`), stored in the host's own `ctx.storage` per session — the landing spot for the LEDGER rule, since OpenCode 2.x gives a plugin no `todowrite`. A repeated ask is ONE item, an id that matches two is refused with both printed, `blocked` carries the reason, and a write that did not reach storage is reported as a failure rather than as recorded | Lead only |
286
+ | `tm_join` | **Sub-agent collection** — there is no plugin-side dispatcher any more (`tm_dispatch` is removed: a child a plugin creates is a session the user can neither open nor stop from the interface). Delegation goes through the host's own `task` / `task { background: true }`, and `tm_join` is the read side: a status snapshot, a bounded `waitMs`, `cancel:true` to stop a runaway, and `claimNamedChild` so `tm_join { ids: ["ses_…"] }` pulls one child's WHOLE reply back through the offload pipeline (handle + ≤80-token preview) instead of kilotokens inline. It also rebuilds its registry from the host session tree after a restart, so leftover children are 接管 rather than lost. **On 2.x it also registers the children the host's own `subagent` tool created** (the ack's `metadata.sessionID` is the seam), so a background dispatch is never reported as "nothing to collect" while the user watches it run — and such a row says where its reply actually arrives (the host's injected message) and whether it settled by event or by inference. **Stopping one is a real call:** `cancel: true` goes to the host's own `ctx.session.interrupt` on 2.x (`tm_join { ids: ["ses_…"], cancel: true }` stops ONE named child; without `ids` it stops every still-running one). The host's own contract is `interrupted=true` for an active execution and `false` for the idle no-op, so the answer comes back as **five distinct verdicts** — 已由宿主中断 / 空闲未中断(it wasn't running: our row was stale, not failed)/ 未确认(the call worked, no boolean came back)/ 无中断缝 / 被宿主拒绝(with the host's reason) — counted in the reply and written to the trajectory (`stop_tried` / `stop_confirmed` / `stop_refused` / `stop_unknown`, readable via `tm_stats`). It will not summarise five different facts as one "已取消", and `resume` is never sent, because resuming pending steering is the opposite of cancelling | Lead only |
287
+ | `tm_stats` | **The plugin reads its own trajectory back**: tokens kept out of the context window by offloading (net of the preview that arrived), seconds saved by dispatch overlap (serial cost minus the wall window the children actually used), governance counts (blocked subresources, clamped shell timeouts, cache hits, redactions) — plus the **host capability matrix** (`已验证/存在未用/待观察/缺失/需人眼` per host surface) and the **layered-config section** (per-key source, red-line keys a file tried to set and lost, unknown keys, skipped layers, auto-create state). Read-only over files this plugin wrote; run it first after an OpenCode upgrade. `{ recent: 20 }` appends a call-by-call recap — handle + payload path for every offloaded result, which is how you see what a governed tool actually returned (the host gives plugin tools no expandable card) | All agents |
288
+
289
+ > **The v1 (1.18.x) personality was removed in full for the 1.7.0 line.** The package
290
+ > exports `{id, setup}` and nothing else, so `tm_read` / `tm_grep` / `tm_bash` /
291
+ > `tm_ptc_run` / `tm_pty` are gone with it — the host's own `read` / `grep` / `glob` /
292
+ > `shell` and its `execute` (Code Mode) do that work now. The governance did NOT
293
+ > disappear with them: an oversized native result is offloaded through
294
+ > `tool.execute.after` (measured: `shell` 12,902 tokens arriving as a 78-token
295
+ > preview), and the address red line plus R6's per-command classification ride the
296
+ > host's `permission.evaluate`. `tm_ledger` is the lead's list where the host gives a
297
+ > plugin no `todowrite`.
298
+ > **Nothing we change reaches outside Team.** Every v2 hook fires for every session on
299
+ > the host, so the tool-surface trim, the 0.2 temperature, the blackboard note, the offload
300
+ > of native results, the R6/address strictening and the forced-background dispatch all ask
301
+ > "is this one of our six roles?" first — `build`, `plan` and your own agents stay as a
302
+ > fresh install leaves them. Calls whose owner the host did not tell us are left alone too,
303
+ > and counted (`tm_stats` prints `作用域:我们 · 他人 · 未判定`).
304
+ > The installer writes the six
305
+ > role prompts, which name `read` / `grep` / `shell` instead of the removed aliases.
306
+
307
+ > **On OpenCode 2.x, interactive browsing is the host's, and it stays ours.** The
308
+ > desktop renders a browser in its side panel, and that panel attaches to the
309
+ > server's own browser service — a plugin cannot register a page of its own into it
310
+ > (forensics: `docs/research/browser-pane.md`). So the three roles with network
311
+ > grants (lead, researcher, tester) are pointed at the host's `browser_*` tools
312
+ > — the self-built `tm_browser` was removed in 1.7.0, so the host's catalog is the
313
+ > only browser. On a host with no native browser catalog (CLI, standalone) there is
314
+ > now no browser at all, and the agent reports that gap rather than simulating one.
315
+ > Handing browsing over did not mean handing
316
+ > governance over: `permission.evaluate` was observed NOT firing for `browser_*`, so
317
+ > our gate sits on `tool.execute.before` instead — it classifies the URL of every
318
+ > navigate/open, the path of every `browser_preview`, and every browser URL named
319
+ > inside an `execute` program, refuses the ones out of policy, and if the host runs a
320
+ > refused call anyway it replaces the page with the same refusal, so no
321
+ > out-of-policy content reaches the context, the store or the trajectory. What we
322
+ > cannot do is un-make a request the host already performed, and the reply says so
323
+ > rather than claiming a block; `tm_stats` prints the two numbers apart (refused /
324
+ > leaked past the refusal). One exception to the offload rule lives here: a
325
+ > `browser_snapshot` is an addressing table, not a document, so it is CAPPED (every
326
+ > `[ref=…]` line kept, static text dropped, budget `nativeSnapshotMaxTokens`
327
+ > default 1 200) rather than replaced by a
328
+ > handle. Measured on a 261-ref page: a head cut keeps 118 refs, this keeps 261 of
329
+ > them using 1 044 of 11 326 tokens.
330
+
331
+ > **Fixed tool priority ladder (every task): ① the user's own MCP/plugin
332
+ > tools → ② TeamMode governed tools (`tm_*`) → ③ the model's own reasoning.**
333
+ > It doubles as the fallback chain: when a tool errors (no browser on this
334
+ > host, blocked host), the agent says so and drops to the next rung — and
335
+ > rung ③ is where a missing capability gets reported, never fabricated.
336
+ > One stated exception: on the **web** channel `tm_search` / `tm_webfetch` come
337
+ > first, because that path is the only one carrying the
338
+ > domain allowlist, the per-request confirmation dialog and the R6 red lines.
339
+
340
+ > **What the host UI cannot show you.** OpenCode renders a *plugin* tool call
341
+ > as a one-line card with no expandable body — its tool-renderer registry
342
+ > holds only the built-in tool names, and an extension cannot add to it. The
343
+ > governed output still exists: anything over the offload boundary is written
344
+ > to disk as a handle, and `tm_stats { recent: 20 }` prints the call-by-call
345
+ > recap with those handles and file paths, which you can open. Ask your agent
346
+ > for it — "what did that tool actually return?"
347
+
348
+ > **Delegation goes through the host's `task` — one channel, two shapes.**
349
+ > OpenCode's own `task` tool is the only sub-agent the interface can SHOW you:
350
+ > its card links to the live child session, and the user can stop it. Add
351
+ > `background: true` and it is also non-blocking — the host wakes your lead when
352
+ > the child finishes. That flag is experimental, so you turn it on yourself:
353
+ >
354
+ > ```
355
+ > OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
356
+ > ```
357
+ >
358
+ > (set it for the app process — `setx` on Windows, or launch from a shell that
359
+ > exports it — then restart OpenCode; the installers do it for you, and that is
360
+ > a **persistent user-scope write**, so see
361
+ > [What the installer changes on your machine](#-what-the-installer-changes-on-your-machine)
362
+ > for the exact value, its lifetime and the undo command). TeamMode
363
+ > then keeps that channel inside your token budget: the injected full reply is
364
+ > replaced by a preview plus a pointer, and nothing is copied to disk
365
+ > (`taskOffload: "off"` restores the host's verbatim text). Cost of the host
366
+ > path, stated plainly: each finished background task wakes the lead and costs a
367
+ > turn. `tm_join` is the read side that stays — it pulls a named child's whole
368
+ > reply back through the offload pipeline, stops a runaway, and rebuilds its
369
+ > registry from the host session tree after a restart.
370
+ >
371
+ > There is deliberately **no plugin-side dispatcher**: an earlier `tm_dispatch`
372
+ > created children the user could neither open from a card nor stop from the
373
+ > interface, which is a worse trade than the batching it bought. Remove it,
374
+ > and nothing breaks — the host's `task` was always the visible path.
375
+
376
+ All governed tools are **parallel-safe**: the host may run a batch of
377
+ `tm_search` / `tm_webfetch` / `tm_fetch` calls concurrently — each call gets
378
+ its own step id and its own payload, and nothing cross-contaminates
379
+ (proven by the parallel test suite).
380
+
381
+ ### Context governance: offload, handles, previews
382
+
383
+ Large tool outputs are context cost's main driver — every step re-sends the
384
+ whole window. So results above the content-class offload boundary (prose →
385
+ `offloadThresholdText`, structured data → `offloadThresholdData`,
386
+ unknown → global `offloadThreshold`) are written to a local run store
387
+ (`<repo>/.git/opencode-team/`, never your working tree) and replaced by a
388
+ handle with a content-aware preview: JSON keys / CSV header + shape / log
389
+ ERROR×N stats / code signatures / binary metadata, hard-capped at 80 tokens
390
+ (`previewMaxTokens`). When the agent actually needs the payload, it pages
391
+ through with `tm_fetch` using an HMAC-signed, run-scoped, expiring handle — and
392
+ on a JSON handle it can ask for a `fields` dot-path projection instead (a
393
+ deliberately small jq subset: `items[].name`, `[].stargazers_count`), so a big
394
+ API dump narrows to just the values needed without the raw body ever entering
395
+ the window.
396
+
397
+ The same pipeline governs the **host's own tools**, not just ours — an
398
+ oversized native result is rewritten at `tool.execute.after` into the same
399
+ preview + handle shape. Report-shaped results are the third outcome: a table
400
+ with a hole in it is not a table, so a result carrying a Markdown table is
401
+ **capped keeping its table lines** (`nativeReportMaxTokens`) and the prose
402
+ between them is what goes.
403
+
404
+ ### Session + project + global memory (tm_memory)
405
+
406
+ Durable facts — build commands, environment quirks, architecture decisions,
407
+ your conventions — live as human-editable Markdown with frontmatter, in
408
+ THREE tiers:
409
+
410
+ - **`session`**: this conversation's transients only — in-process, TTL-swept
411
+ (`memorySessionTtlMin`, default 240 min), invisible to other
412
+ sessions; ephemeral unless `memorySessionPersist: "1"` writes them under
413
+ `memories/sessions/<sid>/`.
414
+ - **`project`** (default): `<repo>/.git/opencode-team/memories/…` — per checkout, git-adjacent. Facts about THIS repo: build commands, environment quirks, architecture decisions.
415
+ - **`global`**: `~/.opencode-team/memories/global/` (override `memoryGlobalDir`) — **follows you across ALL projects**. User-level conventions: preferred package manager, commit style, tooling habits.
416
+
417
+ Actions: `add` / `search` (deterministic keyword scoring) / `list` / `forget` /
418
+ `compact`; 4000 chars per memory. `search` walks ALL tiers with **session >
419
+ project > global precedence** — project entries get a +2 near-tie weight and a
420
+ same-title higher-layer twin shadows the lower one (it never surfaces).
421
+ Near-duplicates never pile up: an `add` that hits an existing entry in the same
422
+ tier and category (dedup key, or title+keywords Jaccard ≥ 0.6) **folds into
423
+ it** — new content wins, keywords union, the old slug moves to `supersedes:`,
424
+ and the answer says 已合并 (that is normal; don't re-add under a variant title).
425
+ When a tier reaches `memoryMaxEntries` (200 per scope) the add fails on
426
+ purpose: run `compact` first — it dry-runs the merge plan by default, and
427
+ re-running with `apply:true` performs it after copying every original to a
428
+ timestamped `.compact-backup` tree (the rollback path). Entries older than
429
+ `memoryStaleDays` (30) surface tagged `[stale Nd]` in search. Agents
430
+ are prompted to search before assuming conventions and to save hard-won
431
+ facts for the next conversation.
432
+
433
+ ### 🌐 Web search that actually works (in China)
434
+
435
+ `tm_search` is the open-ended-lookup front: **one call, one query, clean
436
+ results**. The engine URL is built for you, fetched through the governed
437
+ pipeline, and collapsed into a numbered title+URL hit list — the agent never
438
+ sees raw SERP chrome.
439
+
440
+ | Engine | Notes |
441
+ |---|---|
442
+ | `auto` (default) | Classifies the query, fans the matching engines out **in parallel** (every route has ≥2 legs, because a hit two engines agree on is worth more than one engine's opinion of itself), dedupes by host+path and fuses them with weighted RRF into a top-10 list tagged with each hit's source engine(s). Ranking is scaled by real query-token overlap (floor `searchRelevanceFloor`), so a trusted engine's off-topic junk no longer outranks another engine's best hit. Routing: errors / camelCase APIs → `stackoverflow`+`github`+`bing`; dev-ecosystem (releases, frameworks, open source) → `hn`+`github`+`npm`; Chinese → `bing`+`moegirl`+`stackoverflow`+`hn`; other → `bing`+`stackoverflow`+`hn`+`github`. Pin another default via `searchDefaultEngine` |
443
+ | `bing` | cn.bing.com — the only live CN HTML SERP; multi-word CJK queries get their phrase boundary protected (quoted) so markup shuffle can't split the result list |
444
+ | `bing-int` | The same host with `&ensearch=1` for English results. It is deliberately **out of every `auto` route** — two layouts of one index would give bing a double vote |
445
+ | `stackoverflow` | api.stackexchange.com question search (no key, 300/day/IP) → numbered questions with composite snippets; `auto` tracks the quota and swaps in `bing` once spent |
446
+ | `hn` | Hacker News via Algolia API (no key) → story titles + snippets with direct article URLs |
447
+ | `bilibili` | video search |
448
+ | `moegirl` | MediaWiki search API — entry titles + snippets, structured |
449
+ | `npm` | registry search → name@version + description, structured |
450
+ | `github` | repo search API → stars + description, structured; `org:` / `user:` / `stars:` / `language:` qualifiers fold into the query (e.g. `vector db stars:>500 language:rust`) |
451
+
452
+ All the HTML engines are reachable from mainland China **without API keys**, and
453
+ every one of them sits on the seeded domain allowlist. The previous CN HTML
454
+ SERPs (`sogou` / `so` / `baidu`) were **removed** — a live
455
+ benchmark (2026-09-14) showed them serving anti-bot shells or 100% empty
456
+ results; they are not even manually selectable. On an empty result,
457
+ the error names the alternative engines instead of leaving the agent stuck.
458
+ Two more channels complete the surface:
459
+
460
+ - `tm_webfetch` — a known URL, one governed GET. Search-engine pages it
461
+ fetches are auto-extracted to hit lists too. JSON endpoints like
462
+ `registry.npmjs.org/<pkg>/latest` pass through untouched.
463
+ - **Interactive browsing is the host's own `browser_*` tools** (`browser_tabs_open`
464
+ · `browser_navigate` · `browser_snapshot` · `browser_click` · `browser_evaluate`
465
+ …). The self-built `tm_browser` was **removed in 1.7.0** (a breaking change):
466
+ the desktop renders a browser in its side panel and that panel attaches to the
467
+ server's own browser service, so a plugin cannot register a page of its own
468
+ (forensics: `docs/research/browser-pane.md`). The host catalog is policed by
469
+ `src/host/v2-browser-gate.ts` at `execute.before` — the URL of every navigate/open,
470
+ the path of every `browser_preview`, and every browser URL named inside an
471
+ `execute` program are classified against the address red lines and the env-file
472
+ rule, and a refused call the host runs anyway has its page replaced with the same
473
+ refusal, so no out-of-policy content reaches the context, the store or the
474
+ trajectory. Calling convention differs from the deleted tool: `evaluate` takes
475
+ `{tabID, script}` where `script` is an **expression** (not function source) and
476
+ the return value must be a scalar you `JSON.stringify` yourself; snapshot tokens
477
+ read `@e8 [link]`. **Known cost, accepted:** on a host with no native browser
478
+ catalog (CLI / standalone) there is now no browser at all — the agent reports that
479
+ gap rather than simulating one.
480
+
481
+ When a fetch still returns **403 after the real-Chrome headers**, the error
482
+ is a DIRECTIVE: the gate is JS-challenge / TLS-fingerprint based and only a
483
+ real browser passes — the agent is told to open that URL with the host's
484
+ native `browser_navigate` (and read it with `browser_snapshot`). Search hit lists also
485
+ filter known noise: engine-internal wrappers (`so.com/link?`, `ai.so.com`)
486
+ and same-name-different-site domains (`maimai.cn` 脉脉 vs the maimai DX
487
+ game) never ride along — extend the hit blacklist with `hitBlacklist`.
488
+
489
+ Seeded allowlist (all three web tools; 22 hosts — baidu/moegirl/bilibili are
490
+ PARENT domains, so every sibling subdomain — baike.baidu.com,
491
+ mzh.moegirl.org.cn, space.bilibili.com — is covered):
492
+ `baidu.com`, `bdimg.com` (Baidu's own script + asset CDN — not a baidu
493
+ subdomain, and a page whose bundle we block is a blank page we would then
494
+ report as "no content"), `moegirl.org.cn`, `bilibili.com`, `www.sogou.com`,
495
+ `www.so.com`, `cn.bing.com`, `www.bing.com`, `zhihu.com`, `juejin.cn`,
496
+ `csdn.net`, `cnblogs.com`, `gitee.com`, `github.com`, `api.github.com`,
497
+ `raw.githubusercontent.com`, `gist.githubusercontent.com`, `ghproxy.net`
498
+ (mainland mirror for github raw), `stackoverflow.com`, `npmjs.org`, `pypi.org`,
499
+ `learn.microsoft.com` — plus the two JSON search-engine hosts
500
+ (`api.stackexchange.com`, `hn.algolia.com`) while the default seed is in play,
501
+ so 24 effective. Extend via `webfetchAllowedDomains` (`"*"` opens every host; a
502
+ custom list REPLACES the seed, so keep the engine hosts or `tm_search` loses its
503
+ targets). Architect / implementer / reviewer have NO network grant — web
504
+ questions come back as a reported gap, never simulated. The tester carries the
505
+ host's native `browser_*` tools ONLY, for governed UI verification of the project
506
+ (local dev servers, preview routes); open web fetching stays with the two
507
+ network roles.
508
+
509
+ **Out-of-allowlist targets are a gate, not a wall.** When a fetch / search /
510
+ browser-open points at a host outside the allowlist, the governed call **fails
511
+ closed with a refusal naming the exits** — on 2.x a plugin cannot raise the
512
+ host's dialog, so there is no window to wait for and a gate with no door is not
513
+ a gate. Your doors are `privateSpace: "allow"` in `team-mode.jsonc`, or that one
514
+ hostname in `webfetchAllowedDomains`. Below that gate sit the classes no config
515
+ opens: cloud-metadata / link-local / multicast / reserved / benchmarking ranges
516
+ are refused outright with no consent path, including their IPv4-mapped and
517
+ DNS64 spellings (changing the notation is not a bypass), and env-file URLs and
518
+ non-http(s) schemes are hard-rejected.
519
+
520
+ ### Security: R6 env protection + R2 dangerous operations
521
+
522
+ **R6 is armed by default, and it is a `strict` default.** `envProtect` accepts
523
+ `strict` / `standard` / `off` and resolves to `strict` by default. With TeamMode
524
+ active:
525
+
526
+ - **Env-FILE reads are a hard `deny`** — a Team role's native `read` /
527
+ `edit` / `glob` / `grep` of a `.env` is refused with no consent path. The
528
+ host's `grep` reports the *pattern* as its permission resource rather than the
529
+ path, so the same rule also runs on the whole tool input (`grep SECRET .env`
530
+ cannot slip through). Templates (`*.env.example` / `.sample` / `.template` /
531
+ `.dist`) stay readable.
532
+ - **Env-variable reads in a shell command go to the host's permission prompt**
533
+ (`ask`). The per-command classifier decides, so an ordinary `git status` asks
534
+ nothing and an env dump still does; env reads no wildcard can express
535
+ (embedded `$VAR` / `${VAR}` / `$env:` inside another command, command
536
+ substitution) are **never** asked — they are refused. The audit log records
537
+ only tool name + pattern category + verdict — never command text, paths,
538
+ variable names or values.
539
+
540
+ **R2 dangerous operations (the same face).** Delete, git publish, network
541
+ fetch, package install/publish, process/system, privilege changes — none are
542
+ silently allowed. The normal verification stack (`npm test`, `tsc`,
543
+ `git status`) is NOT gated, so day-to-day work runs uninterrupted.
544
+
545
+ **The plugin never self-allows.** Every hook it registers can only make a rule
546
+ *stricter*; a permission it is asked about is replied `deny` or left alone, never
547
+ `allow`.
548
+
549
+ > ⚠️ **When you approve the host's own dialog, pick "once" — not "always".**
550
+ > Verified on the live host, "always" records a far broader rule than the command
551
+ > you saw: approving `Get-ChildItem env:PATH` with "always" stores `Get-ChildItem
552
+ > *`, so every later `Get-ChildItem` runs with no dialog at all. The web channel
553
+ > has the same trap and it is easier to fall into: one "always" on a host lets
554
+ > **every agent session in that project** open it. The reply says which path let
555
+ > a page in (static allowlist / the dialog you just answered / a saved rule that
556
+ > answered in milliseconds), because an agent that cannot tell them apart reports
557
+ > "no dialog" as "that site is allowed".
558
+
559
+ > Deferral is per-session: the env face only applies in sessions running
560
+ > TeamMode's generated roles. In any other session the plugin leaves the host's
561
+ > behaviour exactly as it found it.
562
+
563
+ ### Repo hygiene
564
+
565
+ The offload/trajectory/memory stores live under `<repo>/.git/opencode-team/`
566
+ (outside a git repo: the OS temp dir, sharded per workspace as
567
+ `opencode-team/w-<hash>/`, so one project never reads another's payloads) —
568
+ **never your working tree**. Every
569
+ agent is instructed to delete scratch files before reporting done and to keep
570
+ throwaway work in the OS temp dir. A TTL sweeper reclaims old task dirs at
571
+ startup + hourly; the Team Lead never deletes boards itself, so you can audit
572
+ any run.
573
+
574
+ ---
575
+
576
+ ## ⚙️ Configuration
577
+
578
+ **Configuration is JSON, and only JSON.** `team-mode.jsonc` is the single
579
+ configuration source. There is no environment layer, and **no `TM_*`
580
+ configuration environment variable exists any more** — the ones you may still
581
+ find in old configs are simply unknown keys, reported at boot and never applied.
582
+ (Three `TM_*` names survive as internal/test switches, listed at the end of
583
+ this section.)
584
+
585
+ ### Where the file lives
586
+
587
+ | Layer | Path | Precedence |
588
+ |---|---|---|
589
+ | **global** | `~/.config/opencode/team-mode.jsonc` (honours the host's `OPENCODE_CONFIG_DIR`) | lowest — applies to every project |
590
+ | **project** | `<dir>/team-mode.jsonc`, for every directory from the workspace **up to the filesystem root** | higher — direct files merge farthest→closest |
591
+ | **project** | `<dir>/.opencode/team-mode.jsonc`, same walk | **highest** — every `.opencode/` file overrides every direct file |
592
+
593
+ That walk and precedence are the host's own config-layering rule, copied rather
594
+ than invented. JSONC is supported: `//` and `/* */` comments are masked at the
595
+ byte level, so a commented-out key cannot read as a live one. A layer that fails
596
+ to parse is skipped **whole** (half a config is worse than none) while a single
597
+ bad key loses only itself; unknown keys are warned about and never applied.
598
+ `tm_stats` renders the result: per-key source, skipped layers, unknown keys, and
599
+ the auto-create state.
600
+
601
+ ### It writes itself
602
+
603
+ When the global file is **absent**, the plugin creates it at boot — atomically,
604
+ idempotently, and **never overwriting an existing file**. What it writes is an
605
+ **inert** template: all **59** registry keys, each as a *comment* under a
606
+ one-line description, so the file parses to `{}` and sets nothing until you
607
+ uncomment what you want.
608
+
609
+ ```jsonc
610
+ // 全局配置。取消注释即可生效;默认全部注释 = 什么都不设置。
611
+ {
612
+ // "offloadThreshold": 2000, // 卸载阈值(估算 token;等于阈值也卸载)。
613
+ // "envProtect": "strict", // R6 环境防护模式(strict/standard/off)。(红线:仅全局)
614
+ // …共 59 个键,每个都带一行说明
615
+ }
616
+ ```
617
+
618
+ Turn it off with the plugin option `autoCreate: false`, or the internal switch
619
+ `TM_CONFIG_AUTOCREATE=off`.
620
+
621
+ ### Red-line keys
622
+
623
+ Five keys are **honored only in the global file**: `envProtect`, `r6FineAsk`,
624
+ `privateSpace`, `webfetchAllowedDomains`, `bashReadonlyAllowed`. A project file
625
+ may set them, but the value is **ignored and reported** — a committed project
626
+ config must not be able to switch off a guard for everyone who clones the repo.
627
+ The rule is derived from the registry itself (`redLine: true` on the spec), so a
628
+ new red-line key is red-line by declaration.
629
+
630
+ ### Plugin options
631
+
632
+ Options live on the plugin entry, not in `team-mode.jsonc`:
633
+
634
+ ```jsonc
635
+ {
636
+ "plugins": [
637
+ { "package": "@te-river/opencode-team-mode@latest",
638
+ "options": { "defaultAgent": true, "autoCreate": true, "ttlDays": 7, "envProtect": true } }
639
+ ]
640
+ }
641
+ ```
642
+
643
+ | Option | Default | Meaning |
644
+ |---|---|---|
645
+ | `defaultAgent` | `true` | Promote Team to the default agent slot. Opt out with `false`. |
646
+ | `autoCreate` | `true` | Write the inert global `team-mode.jsonc` at boot when absent. |
647
+ | `ttlDays` (alias `blackboardTtlDays`) | `5` | Blackboard retention, valid range (0, 365]. |
648
+ | `envProtect` | `true` | Plugin-wide kill switch; `false` resolves the R6 mode to `off`. |
649
+ | `temperature` | `false` | `false` = the documented 0.2 invariant; a number overrides it. |
650
+
651
+ On 2.x a plugin entry is either a **string** or an **object** with `package` and
652
+ `options`. The 1.x tuple form `["@te-river/…", { … }]` is rejected
653
+ (`path=$.plugins.1 kind=invalid`, measured 2026-10-06). Options do arrive — with
654
+ the object form the boot row reads `board_ttl_days=9` for `{ "ttlDays": 9 }`, so
655
+ this is measured, not documented-and-hoped.
656
+
657
+ ### Model choice
658
+
659
+ Every judgment — triage, decomposition, dispatch briefs, synthesis, review
660
+ verdicts — flows through the Team Lead. A weak model in that seat degrades the
661
+ whole pipeline no matter how strong the specialists are. Pin your best reasoning
662
+ model to `team`:
663
+
664
+ ```jsonc
665
+ {
666
+ "agent": {
667
+ "team": { "model": "anthropic/claude-opus-4-5" }, // Lead earns your best model
668
+ "implementer": { "model": "anthropic/claude-sonnet-4-6" } // specialists tolerate cheaper
669
+ }
670
+ }
671
+ ```
672
+
673
+ Your own agents named `team` / `architect` / … always take precedence; the
674
+ plugin never clobbers user definitions. See [Customization](#-customization)
675
+ for overrides, extra agents and disabling roles.
676
+
677
+ ### Key reference (all 59)
678
+
679
+ **Offload, previews and the store**
680
+
681
+ | Key | Type | Default | Meaning |
682
+ |---|---|---|---|
683
+ | `offloadThreshold` | number | `2000` | Global offload boundary (estimated tokens, CJK-aware), used when the content class is unknown. A result AT the threshold offloads. |
684
+ | `offloadThresholdText` | number | `4000` | Boundary for prose (text / log / markdown). Inherits the global value unless set. |
685
+ | `offloadThresholdData` | number | `2000` | Boundary for structured payloads (json / csv / code / binary). Same inheritance. |
686
+ | `previewLines` | number | `20` | Raw lines a preview builder may scan. |
687
+ | `previewMaxTokens` | number | `80` | Hard preview cap. |
688
+ | `fetchMaxLines` | number | `2000` | `tm_fetch` per-segment line cap. |
689
+ | `blackboardDir` | string | auto | Run-payload store dir. Empty = `<repo>/.git/opencode-team/blackboard`. Explicit = absolute or project-relative. |
690
+ | `trajectoryDir` | string | auto | Trajectory store dir. Empty = `<repo>/.git/opencode-team/trajectory`. |
691
+ | `blackboardTtlDays` | number | `7` | Handle TTL and the physical sweep period for expired run dirs. |
692
+ | `storeReclaim` | `on`/`off` | `on` | At boot, reclaim what an upgrade left behind: a per-workspace shard idle past the TTL, and the pre-shard `blackboard/` + `trajectory/` at the temp-dir fallback. **Only TTL-expired entries are ever removed** — a fresh run dir survives, because a session started before the upgrade may still be writing there. `off` leaves the disk exactly as found. |
693
+ | `taskOffload` | `on`/`off` | `on` | Keep the host's background sub-agent inside the context budget: when it finishes, the injected full reply is replaced by a preview + a `tm_join` pointer. Touches only synthetic parts carrying the host's own `<task id=… state="completed">` / `<subagent … state="completed">` envelope and over the text threshold — a typed message is never touched even if it contains a perfect envelope, and a `state="error"` child is never rewritten. **Nothing is copied to disk**: the text stays where the child session already wrote it. `off` restores the host's verbatim injection. |
694
+ | `boardMaxChars` | number | `200000` | `tm_board_write` per-file character cap — over it the write **refuses** (with a "split the topic" hint) rather than truncating a deliverable. |
695
+ | `boardMaxFiles` | number | `200` | Markdown files allowed per session folder. The TTL sweeper is the only reclaim path, so the refusal names it. |
696
+ | `joinMaxWaitMs` | number | `60000` | Ceiling on `tm_join { waitMs }`. Was 300 000, and a lead parked in it twice in a row (19 min of nothing) while its children worked — waiting is not parallelism, so the default now says "check, then work". A second consecutive wait after nothing settled is cut to 10 s and answered with what to do instead. |
697
+ | `ledgerMaxItems` | number | `200` | Ceiling on `tm_ledger` items per session. The host's `ctx.storage` has no TTL and no quota (measured), so the list refuses to grow past this instead of quietly dropping the oldest asks — and a refusal is something the lead can act on, while a silent truncation is a claim nobody can re-check. |
698
+
699
+ **Memory**
700
+
701
+ | Key | Type | Default | Meaning |
702
+ |---|---|---|---|
703
+ | `memoryGlobalDir` | string | auto | GLOBAL tier store. Empty = `~/.opencode-team/memories/global`. |
704
+ | `memorySessionTtlMin` | number | `240` | Session-tier entry TTL (lazy + boot sweep). |
705
+ | `memoryMaxEntries` | number | `200` | Per-scope entry cap; over it `add` fails on purpose — run `compact`. |
706
+ | `memoryStaleDays` | number | `30` | Age after which search hits are tagged `[stale Nd]` (`0` disables). |
707
+ | `memorySessionPersist` | string | `""` | `""` = in-process only; `"1"` also writes session entries under `memories/sessions/<sid>/`. |
708
+
709
+ **Web and search**
710
+
711
+ | Key | Type | Default | Meaning |
712
+ |---|---|---|---|
713
+ | `webfetchAllowedDomains` | string[] | 22-host seed | **red line.** Outbound allowlist (`"*"` = any host; `[]` = deny all). A custom list **replaces** the seed — keep the engine hosts. `"*"` does **not** cover private space: loopback / RFC1918 / CGNAT / `.localhost` still need `privateSpace: "allow"`, and non-routable ranges (169.254.0.0/16 metadata, 0.0.0.0/8, multicast, reserved, plus the IPv4-mapped and DNS64 spellings of the same target) are a hard red line no setting opens. |
714
+ | `searchDefaultEngine` | string | `auto` | Engine when no `engine` arg is given (`auto` = classify + parallel fan-out + RRF fusion; any table name also pins a manual default). |
715
+ | `searchMaxHits` | number | `10` | Hits kept per engine leg and in the fused list. |
716
+ | `searchWeights` | record | `{}` | Per-engine fusion weight overrides, e.g. `{"bing": 0.3}`; anything unset keeps the built-in table. |
717
+ | `searchDisabledEngines` | string[] | `[]` | Engines removed from the roster AND from every `auto` route. |
718
+ | `searchRelevanceFloor` | number | `0.35` | Weight fraction kept by a hit sharing no query token with its title/snippet/host (demotes junk without deleting an engine). |
719
+ | `hitBlacklist` | string[] | `[maimai.cn]` | Extra domains never listed as search hits (same-name-different-site noise like 脉脉). Merged with the built-in entry. |
720
+ | `webCacheTtlSec` | number | `300` | How long a governed fetch may re-serve the same URL (`0` = off). One store shared by `tm_webfetch` / `tm_search`; entries are hash-named (a token-bearing query never hits disk) and are only read or written for hops the STATIC allowlist admitted — so a hit can neither resurrect a removed host nor substitute for consent. A re-served page says 缓存命中. |
721
+
722
+ **Rounds, dispatch and context**
723
+
724
+ | Key | Type | Default | Meaning |
725
+ |---|---|---|---|
726
+ | `prune` | `on`/`off` | `on` | Context pruning: replace a SETTLED message body with a self-explaining pointer (an offload handle, a child id, a note). Evidence is never destroyed — the reply skeleton, GOAL/ACCEPTANCE, provenance, system messages, a still-running child and an uncollected child id are protected whole, and the untouched messages stay byte-exact. `off` disables. |
727
+ | `pruneAtPercent` | number | `70` | Prune once the request reaches this share of the model's window, derived from `limit.context` — never a hard-coded token count. **Clamped to 40–90.** |
728
+ | `pruneKeepTailPercent` | number | `40` | Share of the window kept verbatim at the tail; the newest message is always kept. |
729
+ | `splitAdvice` | `on`/`off` | `on` | Inject a split suggestion when a dispatch brief carries too many acceptance criteria. `off` stops the hint (the prompt discipline stays). |
730
+ | `splitBriefTokens` | number | `4000` | The brief token threshold that triggers it. **Clamped to ≥200.** |
731
+ | `splitMaxCriteria` | number | `3` | More acceptance criteria than this is a signal to split. |
732
+ | `retry` | `on`/`off` | `on` | Retry governor: classify a quota / rate / transient / unknown error **by name and text only**, compute jittered backoff, and inject ONE directive naming the exact seconds to wait. After `retryBreakAfter` consecutive errors a cooldown denies new `subagent` dispatches. Honest boundary: there is no seam to intercept the model's own retry — it identifies, states the wait, and refuses new work; it does not wait for the model. |
733
+ | `retryBaseMs` | number | `5000` | First backoff; doubles per consecutive error. |
734
+ | `retryMaxMs` | number | `60000` | Ceiling for that backoff. |
735
+ | `retryJitter` | number | `0.3` | ± fraction applied to the wait, so parallel sessions do not re-fire together. |
736
+ | `retryBreakAfter` | number | `5` | Consecutive errors before the breaker trips. |
737
+ | `retryCooldownMs` | number | `60000` | How long new sub-agent dispatches are refused once it has. |
738
+
739
+ **Host-surface governance (2.x)**
740
+
741
+ | Key | Type | Default | Meaning |
742
+ |---|---|---|---|
743
+ | `nativeOffload` | `on`/`off` | `on` | Govern the HOST's own `read`/`grep`/`glob`/`shell` results through the same pipeline. `off` restores the host behavior; the boot line distinguishes "you turned it off" from "this host has no execute.after". |
744
+ | `nativeSnapshotMaxTokens` | number | `1200` | Budget a native `browser_snapshot` keeps in context. Addressing lines win it before static text does; past `budget × 4` the reply states how many ref lines did not fit, and kept + dropped always equals the total. |
745
+ | `nativeReportMaxTokens` | number | `1600` | Budget a report-shaped native result (anything carrying a Markdown table) keeps. Table lines and their headings win the space; the full text still rides the handle. |
746
+ | `probeChain` | `on`/`off` | `on` | After a run of native `read`/`grep`/`glob`/`shell` passes, append ONE line naming `execute` (Code Mode) and what it saves. A hint, never a gate: the original body is preserved verbatim, nothing is rejected, and the append is idempotent. |
747
+ | `probeChainAfter` | number | `3` | How many consecutive native calls before the hint starts (`0` = off). |
748
+ | `v2BrowserGate` | `on`/`off` | `on` | The gate over the host's `browser_*` catalog (URLs and preview paths at `execute.before`, plus the browser URLs named inside an `execute` program). `off` restores ungoverned browsing; the boot line and `tm_stats` say which world is running. |
749
+ | `v2CodeMode` | string | `""` | `direct` sends `options.codemode=false`; empty = catalog mode. We used to send `direct` by default and claim direct delivery — **a live 2.0.16 desktop session disproved it**: with the flag sent, every `tm_*` still arrived inside the host's Code Mode catalog, so the default now sends nothing. The observable truth is `tools_in_request` in `tm_stats` (请求内实际可见=…), never the flag we sent. |
750
+ | `bashTimeoutProbeMs` | number | `60000` | Ceiling forced onto a `timeout` the model set for a read-only probe command. Applies ONLY to a command the read-only allowlist already accepts, and never invents a timeout the model omitted (`0` disables). |
751
+ | `bashTimeoutMaxMs` | number | `0` | Optional global ceiling for every other shell command. `0` = off, so a real build keeps the timeout it asked for. |
752
+ | `envProtect` | `strict`/`standard`/`off` | `strict` | **red line.** R6 mode. `off` also disarms the shell escalation. Any unrecognised value resolves to `strict`. |
753
+ | `r6FineAsk` | string | classifier | **red line.** `off` falls back to asking about EVERY shell command — which is also what happens automatically on a host that does not expose the per-action evaluation hook, and the boot note says which of the two caused it. |
754
+ | `privateSpace` | `allow`/`deny`/`ask` | `deny` | **red line.** Private space (loopback, RFC1918, ULA, CGNAT, `.localhost`) through our tools. The default is `deny` because a 2.x plugin cannot raise a dialog — telling the agent to wait for a window that will never open is not a gate with a procedure, so `ask` is the honest spelling of "not answerable" and an unrecognised value falls back to it. Never conflated with the FORBIDDEN ranges. |
755
+ | `bashReadonlyAllowed` | string[] | built-in | **red line.** The P3 read-only command allowlist. `tasklist` / `ps` / `findstr` are seeded in, because `已确认关闭` is only checkable if the agent can ask the OS for the pid — a user-facing verification with no read path is the same defect as a lie. |
756
+
757
+ **Legacy keys** — kept listed and parseable so an old config still loads, with
758
+ **no v2 reader**, and reported as such:
759
+
760
+ | Key | Default | Status |
761
+ |---|---|---|
762
+ | `askTimeoutFloorMin` | `1` | **Orphaned.** Its only consumer was the v1 approval gate, which was deleted (a 2.x plugin cannot raise a dialog, so there was nothing to time out on). Say so rather than leaving a knob that does nothing. |
763
+ | `agentTemperature` | `""` | v1-only |
764
+ | `compactionContext` | `on` | v1-only |
765
+ | `shellEnv` | `""` | v1-only |
766
+ | `ptcWebBridge` | `on` | v1-only |
767
+
768
+ > **Compaction timing belongs to the host, not to this plugin.** An earlier
769
+ > release triggered its own summarization at 75% of the window; that was removed
770
+ > by user decision — a plugin that summarizes a live conversation by itself
771
+ > takes a context-losing action you did not ask for. What remains is additive and
772
+ > runs only when the HOST compacts: the must-survive list (reply skeleton,
773
+ > offload handles, open sub-agent session ids, provenance, board paths) rides the
774
+ > host's own `session.hook("compaction")`, and the host's summarizer prompt is
775
+ > never replaced.
776
+
777
+ ### The three surviving `TM_*` names
778
+
779
+ These are **internal / test switches**, not configuration — they are read from
780
+ the environment because a test or a diagnostic needs them without a config file:
781
+
782
+ | Name | Effect |
783
+ |---|---|
784
+ | `TM_STORE_RECLAIM` | Overrides the `storeReclaim` key (`off` = leave the disk exactly as found). The test runner sets it, because suites boot real runtimes in temp dirs. |
785
+ | `TM_V2_PROBE` | Path to a JSONL file where the surface probe records the host's real tool ids, permission action names and argument key **names**. Names and counts only — never a command line, path, URL or env value. It is how "does the host actually have X?" gets answered from the running build instead of from a doc. |
786
+ | `TM_CONFIG_AUTOCREATE` | `off` suppresses the inert global-file auto-create. |
787
+
788
+ Any **other** `TM_*` name in your environment is inert. If you have been setting
789
+ one, move it into `team-mode.jsonc` under the matching key above.
790
+
791
+ ---
792
+
793
+ ## 🏗️ How the team works
794
+
795
+ Sub-agents can't message each other live (platform limitation), so TeamMode
796
+ coordinates them through a **structured reply skeleton** — every specialist
797
+ reply is `STATUS: / CHANGES: / FINDINGS: / EVIDENCE: / HANDOFF:`, ≤50 lines,
798
+ relayed verbatim by the Lead into the next dispatch. Files are the exception,
799
+ not the rule: a deliverable over ~50 lines goes to ONE named board file under
800
+ `<repo>/.git/opencode-team/` (round-suffixed, working tree untouched).
801
+
802
+ - **Routing table:** question → direct answer; docs-only → implementer;
803
+ product change → implementer → tester → reviewer; multi-module →
804
+ architect → implementer → tester → reviewer(s); unknown tech → researcher
805
+ first. Fixed minimums — a product change routed below 3 dispatches is a
806
+ routing bug.
807
+ - **One dispatch, one verifiable deliverable.** A dispatch is a unit of
808
+ VERIFIABLE work, not a bucket for everything related. If the lead cannot say
809
+ how a child's result will be verified *on its own*, it is not a dispatch yet —
810
+ it is a wish. Cutting one deliverable into pieces that only make sense
811
+ together is chopping, not splitting, and it costs you more rounds than it
812
+ saves.
813
+ - **The architect is conditional.** Dispatch it only when the design is
814
+ genuinely unknown. With the root cause already verified (file:line evidence),
815
+ skip it and dispatch the implementer with the exact fix spec; if the fix moves
816
+ a contract or the strategy is undecided, keep the architect. The ceremony
817
+ serves unknowns, not multi-file diffs.
818
+ - **Tester and reviewer may run in the SAME round** — verification and review
819
+ are independent of each other. Serialise them only when a fix invalidates
820
+ both. Same for multiple implementers (each carrying its exact file ownership
821
+ and verbatim data contracts) and testers on disjoint packages.
822
+ - **Approval gate (count-based):** ≥2 dispatches → plan (≤30 lines) → **your
823
+ approval** → execute. Blocking questions are batched and asked immediately.
824
+ Splitting one ask into sub-2-dispatch pieces to dodge the gate is a protocol
825
+ violation, not a trick.
826
+ - **Only the Team Lead dispatches.** Specialists no longer hold the host's
827
+ `task`/`subagent` capability, so no sub-agent spawns a sub-agent.
828
+ - **Adaptive review:** one reviewer by default; three parallel dimensions
829
+ only for high-risk profiles (auth/security, cross-module contracts, public APIs).
830
+ - **Static verification:** build / typecheck / lint / tests. Improvised
831
+ browser automation is banned; unverified UI work ends with
832
+ `UI NOT VERIFIED: <what to check>`.
833
+ - **Evidence standard:** "done / fixed / passed" claims need verifiable
834
+ evidence — output, logs, diffs.
835
+ - **Your language, not the tool's.** The governed tools answer in Chinese; the
836
+ reply-language rule keeps your own language in the prose and quotes a Chinese
837
+ string verbatim only where it IS the evidence.
838
+
839
+ ---
840
+
841
+ ## 🔧 Customization
842
+
843
+ **Override an agent** — same name in your config wins:
844
+
845
+ ```jsonc
846
+ {
847
+ "agent": {
848
+ "reviewer": {
849
+ "model": "anthropic/claude-sonnet-4-6",
850
+ "prompt": "You are an extremely strict reviewer. Reject anything with a lint warning."
851
+ }
852
+ }
853
+ }
854
+ ```
855
+
856
+ **Add your own agents** alongside the team:
857
+
858
+ ```jsonc
859
+ {
860
+ "agent": {
861
+ "devops": {
862
+ "mode": "subagent",
863
+ "description": "Handles CI/CD, Docker, and deployment tasks.",
864
+ "prompt": "You are the DevOps engineer..."
865
+ }
866
+ }
867
+ }
868
+ ```
869
+
870
+ **Disable one:** `"researcher": { "disable": true }`.
871
+
872
+ Board retention, auto-create, the default slot and the R6 kill switch are
873
+ plugin-entry `options` — see
874
+ [Configuration](#️-configuration).
875
+
876
+ ---
877
+
878
+ ## ❓ Troubleshooting
879
+
880
+ **Will this eat my tokens?**
881
+ The opposite is the point. Offload + 80-token previews + the host's Code Mode
882
+ `execute` exist because a five-agent pipeline naively bolted onto one context window
883
+ *would* eat your tokens. The governance is the token-saver.
884
+
885
+ **Is the web access safe?**
886
+ It's the most guarded surface in the plugin: two full web roles plus a
887
+ browser-only tester grant, a domain allowlist whose off-list calls fail closed,
888
+ redirects re-checked per hop with the whole chain reported, env-file URL
889
+ refusal, address red lines no setting opens, and every payload rides the same
890
+ offload governance. No allowlisted page can bounce the fetch off-site.
891
+
892
+ **Why doesn't the plugin auto-update?**
893
+ OpenCode caches plugins by spec string and never re-resolves `@latest`
894
+ (upstream limitation, not ours). **Re-run the installer — that IS the
895
+ update** (it purges the cache and re-resolves npm copies); or delete the
896
+ cache dir by hand. If the boot probe's `entrypoint=` line names a path outside
897
+ `~/.cache/opencode/npm` or `~/.config/opencode/node_modules`, a `~/package.json`
898
+ is pinning a different copy — that is the diagnostic, not a guess. Recipe above
899
+ and in [docs/installation-v2.md](./docs/installation-v2.md).
900
+
901
+ **A knob I set in an env var does nothing.**
902
+ Configuration is JSON now. Any `TM_*` variable other than the three internal
903
+ switches is inert; move the value into `team-mode.jsonc` under the registry key.
904
+ Run `tm_stats` and read the layered-config section: it names the per-key source,
905
+ so a value you expected to be honored shows up with a source that is not the file
906
+ you edited.
907
+
908
+ **A red-line key I set in the project file is ignored.**
909
+ By design. The five red-line keys are honored only in the **global** file, and
910
+ `tm_stats` reports the ignored attempt by name rather than dropping it silently.
911
+
912
+ **My `AGENT NOT FOUND: "Team"`.**
913
+ The file name IS the agent id, and Windows/APFS keep the existing spelling when
914
+ a write opens that file through another one — so a pre-1.7.2 `team.md` survives
915
+ every install and the host registers the lead as lowercase. Use `--agent team`.
916
+ The installer's reclaim pass renames a case-only entry it has proven is the same
917
+ file; a manual install may need the same rename.
918
+
919
+ **Can agents run tools in parallel?**
920
+ Yes — and they're *engineered* for it: parallel `tm_search` / `tm_webfetch` /
921
+ `tm_fetch` calls get distinct step ids and isolated payloads. A regression
922
+ here fails the test suite before it ever reaches you.
923
+
924
+ **Does it work in the CLI (TUI), or only Desktop?**
925
+ Both. Desktop adds the color-coded picker and panels; the governed tools and
926
+ the whole workflow are host-agnostic. Interactive browsing is the host's own
927
+ `browser_*` catalog, so a host with no native browser (CLI / standalone) has
928
+ none — the agent reports that gap rather than simulating one.
929
+
930
+ **What happens if I don't answer the host's permission prompt?**
931
+ The host decides that, not this plugin. The plugin can only refuse: it never
932
+ self-allows, and every guard it installs can only make a rule stricter. A
933
+ session it does not recognize as one of its own is left exactly as the host
934
+ behaved.
935
+
936
+ ---
937
+
938
+ ## 🗑️ Uninstall
939
+
940
+ 1. Remove the entry from the `"plugins"` array in your config file; if you installed on 2.x, delete the generated `~/.config/opencode/agents/*.md` and `commands/team-*.md` too, and any `default_agent: "team"` you no longer want.
941
+ 2. Delete the cache dir (table in [Install](#-read-this-once-save-yourself-an-hour-later)) if you want the disk space back.
942
+ 3. Restart OpenCode. The agents, commands and tools are gone; the stores under `<repo>/.git/opencode-team/` (and `~/.opencode-team/` for global memories) are plain files you can delete whenever. The global `team-mode.jsonc` is yours to keep or delete — the plugin will simply write a fresh inert one if you remove it.
943
+
944
+ No DLLs were harmed. Nothing was written to your working tree.
945
+
946
+ ---
947
+
948
+ ## 🏛️ Architecture (for the curious)
949
+
950
+ ```
951
+ opencode-team-mode/
952
+ ├── src/
953
+ │ ├── index.ts ← Plugin entry ({id, setup} — v2 only)
954
+ │ ├── agents.ts ← Agent structure (modes, colors, temperatures, whitelist matrix)
955
+ │ ├── prompts/ ← Agent prompts (lead / specialists / shared) — pinned by tests
956
+ │ ├── commands.ts ← Slash command definitions
957
+ │ ├── blackboard.ts ← Shared blackboard + TTL sweeper
958
+ │ ├── envprotect.ts ← R6 facade (patterns / classifiers / gate predicates / hook)
959
+ │ ├── identity.ts ← Agent-name identity (case-insensitive lead match)
960
+ │ ├── tm/ ← Governed tools: pipelines / store / preview / guard / refs /
961
+ │ │ config-layers (the 59-key registry) / config-files (the two
962
+ │ │ layers) / config-template (the inert auto-created file) /
963
+ │ │ webfetch / search / memory / board / ledger / dispatch
964
+ │ ├── host/ ← v2 personality (setup / guard / offload / session / events / …)
965
+ │ └── types.ts ← Loader-contract types
966
+ ├── docs/installation.md ← Historical 1.18.x guide (no longer supported)
967
+ ├── docs/installation-v2.md ← The 2.x install guide (roles arrive as generated config files)
968
+ ├── scripts/ ← One-line installers (bash / PowerShell)
969
+ ├── pt07/ ← PT-07 baseline suite (seeded A/B token measurement)
970
+ └── README.*.md ← You are here (twice)
971
+ ```
972
+
973
+ The loader calls `setup(ctx)` once: it registers the `tm_*` tools, resolves the
974
+ two config layers, installs the R6/address guard on `permission.evaluate`, attaches
975
+ the JIT offload on `tool.execute.after`, and publishes the blackboard note. User-defined
976
+ agents with the same name always win — the plugin never clobbers.
977
+
978
+ The one rule that makes the config layer trustworthy: **`CONFIG_KEYS` is the
979
+ registry and the single source of truth.** A key absent from it is reported as
980
+ unknown and never applied; the red-line set is derived from that registry rather
981
+ than hand-listed; and the auto-created global file is rendered *from* the
982
+ registry, so it cannot drift from what the code actually reads.
983
+
984
+ ---
985
+
986
+ ## 🤝 Contributing
987
+
988
+ Issues and PRs welcome. Especially wanted: localization of agent prompts,
989
+ more agent roles, more command templates, and real-world reports of the
990
+ search engines' behavior (they rearrange their markup; the extractor filters
991
+ are intentionally loose but not psychic).
992
+
993
+ ---
994
+
995
+ ## 📄 License
996
+
997
+ [Apache License 2.0](./LICENSE)
998
+
999
+ ---
1000
+
1001
+ ## 🔗 Links
1002
+
1003
+ - [npm Package](https://www.npmjs.com/package/@te-river/opencode-team-mode) — `@te-river/opencode-team-mode`
1004
+ - [Installation guide, **OpenCode 2.x**](./docs/installation-v2.md) — the supported install: a 2.x plugin cannot create an agent, so the six roles and six commands are generated into the config directory
1005
+ - [Installation guide, 1.18.x](./docs/installation.md) — **historical only**: the 1.18.x personality was removed in the 1.7.0 line, so this page is kept for reference and is not a working install path
1006
+ - [OpenCode Desktop](https://opencode.ai) — Official website & download
1007
+ - [OpenCode Docs](https://opencode.ai/docs) — Configuration & plugin documentation
1008
+ - [OpenCode Plugin API](https://opencode.ai/docs/plugins) — Build your own plugins