@te-river/opencode-team-mode 1.5.15 → 1.6.1

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 (196) hide show
  1. package/README.md +853 -690
  2. package/README.zh-CN.md +761 -631
  3. package/dist/agents.d.ts +5 -3
  4. package/dist/agents.d.ts.map +1 -1
  5. package/dist/agents.js +19 -8
  6. package/dist/agents.js.map +1 -1
  7. package/dist/capabilities.d.ts +1 -1
  8. package/dist/capabilities.js +2 -2
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/host/note.d.ts +7 -0
  11. package/dist/host/note.d.ts.map +1 -0
  12. package/dist/host/note.js +28 -0
  13. package/dist/host/note.js.map +1 -0
  14. package/dist/host/v1.d.ts +14 -0
  15. package/dist/host/v1.d.ts.map +1 -0
  16. package/dist/host/v1.js +340 -0
  17. package/dist/host/v1.js.map +1 -0
  18. package/dist/host/v2-browser-gate.d.ts +45 -0
  19. package/dist/host/v2-browser-gate.d.ts.map +1 -0
  20. package/dist/host/v2-browser-gate.js +319 -0
  21. package/dist/host/v2-browser-gate.js.map +1 -0
  22. package/dist/host/v2-capabilities.d.ts +68 -0
  23. package/dist/host/v2-capabilities.d.ts.map +1 -0
  24. package/dist/host/v2-capabilities.js +146 -0
  25. package/dist/host/v2-capabilities.js.map +1 -0
  26. package/dist/host/v2-client.d.ts +53 -0
  27. package/dist/host/v2-client.d.ts.map +1 -0
  28. package/dist/host/v2-client.js +193 -0
  29. package/dist/host/v2-client.js.map +1 -0
  30. package/dist/host/v2-events.d.ts +56 -0
  31. package/dist/host/v2-events.d.ts.map +1 -0
  32. package/dist/host/v2-events.js +158 -0
  33. package/dist/host/v2-events.js.map +1 -0
  34. package/dist/host/v2-guard.d.ts +101 -0
  35. package/dist/host/v2-guard.d.ts.map +1 -0
  36. package/dist/host/v2-guard.js +199 -0
  37. package/dist/host/v2-guard.js.map +1 -0
  38. package/dist/host/v2-offload.d.ts +148 -0
  39. package/dist/host/v2-offload.d.ts.map +1 -0
  40. package/dist/host/v2-offload.js +288 -0
  41. package/dist/host/v2-offload.js.map +1 -0
  42. package/dist/host/v2-permissions.d.ts +123 -0
  43. package/dist/host/v2-permissions.d.ts.map +1 -0
  44. package/dist/host/v2-permissions.js +190 -0
  45. package/dist/host/v2-permissions.js.map +1 -0
  46. package/dist/host/v2-probe.d.ts +85 -0
  47. package/dist/host/v2-probe.d.ts.map +1 -0
  48. package/dist/host/v2-probe.js +268 -0
  49. package/dist/host/v2-probe.js.map +1 -0
  50. package/dist/host/v2-scope.d.ts +51 -0
  51. package/dist/host/v2-scope.d.ts.map +1 -0
  52. package/dist/host/v2-scope.js +58 -0
  53. package/dist/host/v2-scope.js.map +1 -0
  54. package/dist/host/v2-session-client.d.ts +74 -0
  55. package/dist/host/v2-session-client.d.ts.map +1 -0
  56. package/dist/host/v2-session-client.js +171 -0
  57. package/dist/host/v2-session-client.js.map +1 -0
  58. package/dist/host/v2-session.d.ts +85 -0
  59. package/dist/host/v2-session.d.ts.map +1 -0
  60. package/dist/host/v2-session.js +171 -0
  61. package/dist/host/v2-session.js.map +1 -0
  62. package/dist/host/v2-subagent.d.ts +157 -0
  63. package/dist/host/v2-subagent.d.ts.map +1 -0
  64. package/dist/host/v2-subagent.js +286 -0
  65. package/dist/host/v2-subagent.js.map +1 -0
  66. package/dist/host/v2-tool.d.ts +67 -0
  67. package/dist/host/v2-tool.d.ts.map +1 -0
  68. package/dist/host/v2-tool.js +209 -0
  69. package/dist/host/v2-tool.js.map +1 -0
  70. package/dist/host/v2-transcript.d.ts +73 -0
  71. package/dist/host/v2-transcript.d.ts.map +1 -0
  72. package/dist/host/v2-transcript.js +151 -0
  73. package/dist/host/v2-transcript.js.map +1 -0
  74. package/dist/host/v2-types.d.ts +229 -0
  75. package/dist/host/v2-types.d.ts.map +1 -0
  76. package/dist/host/v2-types.js +17 -0
  77. package/dist/host/v2-types.js.map +1 -0
  78. package/dist/host/v2.d.ts +42 -0
  79. package/dist/host/v2.d.ts.map +1 -0
  80. package/dist/host/v2.js +706 -0
  81. package/dist/host/v2.js.map +1 -0
  82. package/dist/host-hooks.d.ts +13 -2
  83. package/dist/host-hooks.d.ts.map +1 -1
  84. package/dist/host-hooks.js +18 -5
  85. package/dist/host-hooks.js.map +1 -1
  86. package/dist/index.d.ts +23 -17
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +29 -326
  89. package/dist/index.js.map +1 -1
  90. package/dist/prompts/lead.d.ts +1 -1
  91. package/dist/prompts/lead.d.ts.map +1 -1
  92. package/dist/prompts/lead.js +452 -359
  93. package/dist/prompts/lead.js.map +1 -1
  94. package/dist/prompts/shared.d.ts +2 -2
  95. package/dist/prompts/shared.d.ts.map +1 -1
  96. package/dist/prompts/shared.js +89 -17
  97. package/dist/prompts/shared.js.map +1 -1
  98. package/dist/prompts/specialists.d.ts +2 -2
  99. package/dist/prompts/specialists.d.ts.map +1 -1
  100. package/dist/prompts/specialists.js +44 -10
  101. package/dist/prompts/specialists.js.map +1 -1
  102. package/dist/task-offload.d.ts +83 -0
  103. package/dist/task-offload.d.ts.map +1 -0
  104. package/dist/task-offload.js +174 -0
  105. package/dist/task-offload.js.map +1 -0
  106. package/dist/tm/args-schema.d.ts +4 -0
  107. package/dist/tm/args-schema.d.ts.map +1 -1
  108. package/dist/tm/args-schema.js +36 -3
  109. package/dist/tm/args-schema.js.map +1 -1
  110. package/dist/tm/board.d.ts +113 -0
  111. package/dist/tm/board.d.ts.map +1 -0
  112. package/dist/tm/board.js +275 -0
  113. package/dist/tm/board.js.map +1 -0
  114. package/dist/tm/browser.d.ts +325 -6
  115. package/dist/tm/browser.d.ts.map +1 -1
  116. package/dist/tm/browser.js +1360 -135
  117. package/dist/tm/browser.js.map +1 -1
  118. package/dist/tm/config.d.ts +36 -25
  119. package/dist/tm/config.d.ts.map +1 -1
  120. package/dist/tm/config.js +29 -7
  121. package/dist/tm/config.js.map +1 -1
  122. package/dist/tm/dispatch.d.ts +133 -53
  123. package/dist/tm/dispatch.d.ts.map +1 -1
  124. package/dist/tm/dispatch.js +481 -279
  125. package/dist/tm/dispatch.js.map +1 -1
  126. package/dist/tm/dupe-guard.d.ts +16 -3
  127. package/dist/tm/dupe-guard.d.ts.map +1 -1
  128. package/dist/tm/dupe-guard.js +27 -7
  129. package/dist/tm/dupe-guard.js.map +1 -1
  130. package/dist/tm/egress.d.ts +44 -0
  131. package/dist/tm/egress.d.ts.map +1 -0
  132. package/dist/tm/egress.js +191 -0
  133. package/dist/tm/egress.js.map +1 -0
  134. package/dist/tm/guard.js +1 -1
  135. package/dist/tm/guard.js.map +1 -1
  136. package/dist/tm/index.d.ts +67 -2
  137. package/dist/tm/index.d.ts.map +1 -1
  138. package/dist/tm/index.js +228 -20
  139. package/dist/tm/index.js.map +1 -1
  140. package/dist/tm/ledger.d.ts +114 -0
  141. package/dist/tm/ledger.d.ts.map +1 -0
  142. package/dist/tm/ledger.js +308 -0
  143. package/dist/tm/ledger.js.map +1 -0
  144. package/dist/tm/perm-ask.d.ts +45 -5
  145. package/dist/tm/perm-ask.d.ts.map +1 -1
  146. package/dist/tm/perm-ask.js +107 -7
  147. package/dist/tm/perm-ask.js.map +1 -1
  148. package/dist/tm/pipelines.d.ts.map +1 -1
  149. package/dist/tm/pipelines.js +24 -2
  150. package/dist/tm/pipelines.js.map +1 -1
  151. package/dist/tm/preview.d.ts +55 -0
  152. package/dist/tm/preview.d.ts.map +1 -1
  153. package/dist/tm/preview.js +225 -0
  154. package/dist/tm/preview.js.map +1 -1
  155. package/dist/tm/ptc/tool.d.ts.map +1 -1
  156. package/dist/tm/ptc/tool.js +2 -4
  157. package/dist/tm/ptc/tool.js.map +1 -1
  158. package/dist/tm/pty.d.ts.map +1 -1
  159. package/dist/tm/pty.js +6 -3
  160. package/dist/tm/pty.js.map +1 -1
  161. package/dist/tm/search.d.ts +5 -0
  162. package/dist/tm/search.d.ts.map +1 -1
  163. package/dist/tm/search.js +40 -8
  164. package/dist/tm/search.js.map +1 -1
  165. package/dist/tm/serp-loop.d.ts +45 -0
  166. package/dist/tm/serp-loop.d.ts.map +1 -0
  167. package/dist/tm/serp-loop.js +80 -0
  168. package/dist/tm/serp-loop.js.map +1 -0
  169. package/dist/tm/stats.d.ts +26 -0
  170. package/dist/tm/stats.d.ts.map +1 -1
  171. package/dist/tm/stats.js +194 -4
  172. package/dist/tm/stats.js.map +1 -1
  173. package/dist/tm/webfetch.d.ts +56 -0
  174. package/dist/tm/webfetch.d.ts.map +1 -1
  175. package/dist/tm/webfetch.js +187 -19
  176. package/dist/tm/webfetch.js.map +1 -1
  177. package/dist/tool-coerce.d.ts +24 -0
  178. package/dist/tool-coerce.d.ts.map +1 -0
  179. package/dist/tool-coerce.js +63 -0
  180. package/dist/tool-coerce.js.map +1 -0
  181. package/dist/types.d.ts +7 -1
  182. package/dist/types.d.ts.map +1 -1
  183. package/docs/installation-v2.md +436 -0
  184. package/docs/installation.md +81 -2
  185. package/docs/research/agent-data-exchange.md +117 -0
  186. package/docs/research/browser-pane.md +89 -0
  187. package/docs/research/host-http-api.md +132 -0
  188. package/docs/research/host-subagent-injection.md +187 -0
  189. package/docs/research/plugin-loader-contract.md +261 -0
  190. package/docs/research/v2-builtin-tools.md +447 -0
  191. package/docs/v2-port-notes.md +315 -0
  192. package/index.js +7 -0
  193. package/package.json +4 -1
  194. package/scripts/gen-v2-config.mjs +304 -0
  195. package/scripts/lib/config-surgery.cjs +239 -0
  196. package/scripts/lib/fake-ctx.mjs +146 -0
package/README.md CHANGED
@@ -1,690 +1,853 @@
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 following
23
- > https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation.md
24
- > Then verify the install using the checks in that guide.
25
- > ```
26
- >
27
- > **The one command to remember afterwards:** `/team-run <task>` — the Team Lead
28
- > plans it, shows you the plan, waits for your approval, then runs the whole team.
29
-
30
- Everything else below is detail. When you're ready for it, here's the map:
31
-
32
- **[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)**
33
-
34
- ---
35
-
36
- ## 🤔 Why TeamMode?
37
-
38
- Because a single agent doing everything is how you get: a context window stuffed
39
- with 5000-line file dumps, twenty round-trips to `bash` for what one script could
40
- do, sub-agents that silently read `.env`, and web "research" that is really just
41
- the model's imagination.
42
-
43
- TeamMode's answer to each:
44
-
45
- | Pain | TeamMode's answer |
46
- |---|---|
47
- | 🔥 **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. |
48
- | 🐌 **Round-trip overhead** | `tm_ptc_run`: the agent writes ONE program that makes N governed calls in a single turn. Zero LLM round-trips during the run. |
49
- | 🕳️ **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. |
50
- | 🌫️ **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. |
51
- | 🧭 **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 host does not draw mermaid, so no agent pretends it does. |
52
- | 🎯 **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. |
53
-
54
- And the workflow discipline underneath: deterministic routing, a ≤30-line plan
55
- you approve before ≥2 dispatches execute, structured `STATUS/CHANGES/FINDINGS/
56
- EVIDENCE/HANDOFF` replies between agents, and static verification (build /
57
- typecheck / tests) instead of vibes.
58
-
59
- That discipline is also why the recommendation is **medium-to-large
60
- projects**: on a two-file script the team simply has less to govern.
61
-
62
- ---
63
-
64
- ## 👥 The team
65
-
66
- | Agent | Role | When to use |
67
- |---|---|---|
68
- | 🎯 **Team Lead** (`@team`) | Orchestrator | Complex tasks that need planning + multi-step execution |
69
- | 🏗️ **Architect** | System designer | Design docs, module structure, API contracts |
70
- | 💻 **Implementer** | Code writer | Building features, writing production code |
71
- | 🔍 **Reviewer** | Dimension-focused auditor | Single-dimension review by default; 3 in parallel only for high-risk changes |
72
- | 🧪 **Tester** | Test engineer | Tests with real edge cases; static verification (build / typecheck / lint); governed UI verification via `tm_browser` |
73
- | 🔎 **Researcher** | Knowledge finder | Local repo first, then the web — one of the two network roles (with the Lead) |
74
-
75
- Out of the box, **Team is your default agent** — new chats open straight into the
76
- orchestrator (opt-out in [Configuration](#-configuration)).
77
-
78
- ---
79
-
80
- ## 📦 Install
81
-
82
- ### Option 1 — Let your agent do it (recommended)
83
-
84
- Copy this into any coding agent — it will edit your config, restart-remind you, and verify:
85
-
86
- ```text
87
- Install the OpenCode plugin @te-river/opencode-team-mode following
88
- https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation.md
89
- (If that URL is unreachable — common on mainland-China networks — retry with
90
- the mirror prefix: https://ghproxy.net/ + the same path.)
91
- Then verify the install using the checks in that guide.
92
- ```
93
-
94
- (The guide is the complete manual procedure — config file locations, plugin
95
- entry, restart, verification, update and uninstall. Your agent reads it and
96
- executes it faithfully; there is nothing else it needs.)
97
-
98
- ### Option 2 — One-line script
99
-
100
- macOS / Linux (bash):
101
-
102
- ```bash
103
- curl -fsSL https://ghproxy.net/https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/scripts/install.sh | bash
104
- ```
105
-
106
- Windows (PowerShell):
107
-
108
- ```powershell
109
- irm https://ghproxy.net/https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/scripts/install.ps1 | iex
110
- ```
111
-
112
- ### Option 3 — Manual
113
-
114
- Add the plugin to your `opencode.jsonc`:
115
-
116
- ```jsonc
117
- {
118
- "$schema": "https://opencode.ai/config.json",
119
- "plugin": [
120
- "@te-river/opencode-team-mode@latest"
121
- ]
122
- }
123
- ```
124
-
125
- OpenCode installs the plugin on next startup.
126
-
127
- ### ⚠️ Read this once, save yourself an hour later
128
-
129
- - **Restart to activate.** After touching `opencode.json`, fully quit and restart OpenCode (Desktop: quit from tray, not just the window).
130
- - **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:
131
-
132
- | OS | Purge command — recursive, covers BOTH cache layouts |
133
- |---|---|
134
- | macOS / Linux | `find ~/.cache/opencode/packages -type d -name '*opencode-team-mode*' -prune -exec rm -rf {} +` |
135
- | 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 } }` |
136
-
137
- ⚠️ 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).
138
-
139
- 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).
140
- - **Prerequisites:** [OpenCode](https://opencode.ai) (Desktop or CLI) and Node ≥ 18.
141
-
142
- ### Verify
143
-
144
- Restart, open the agent picker, and look for **team, architect, implementer,
145
- reviewer, tester, researcher**. That's it — the team is hired.
146
-
147
- ---
148
-
149
- ## 📖 Usage
150
-
151
- ### Slash commands
152
-
153
- | Command | Agent | Description |
154
- |---|---|---|
155
- | `/team-plan <task>` | Architect | Implementation plan with architecture, file manifest, task breakdown |
156
- | `/team-implement <task>` | Implementer | Production code for a feature or task |
157
- | `/team-review [scope]` | Reviewer | Audit for bugs, security issues, quality problems |
158
- | `/team-test [scope]` | Tester | Comprehensive tests with edge-case coverage |
159
- | `/team-research <topic>` | Researcher | Local repo first; web via the governed tools |
160
- | `/team-run <task>` | Team Lead | **The full workflow** — plan → approval → orchestration → verification |
161
-
162
- Or `@`-mention agents directly: `@team`, `@architect`, `@implementer`,
163
- `@reviewer`, `@tester`, `@researcher`.
164
-
165
- ### What a run feels like
166
-
167
- Team is the default agent out of the box, so you just type into a fresh chat:
168
-
169
- > **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/`.
170
-
171
- ```text
172
- team Route: product behavior change → implementer → tester → reviewer
173
- (3 dispatches → approval gate applies)
174
-
175
- PLAN
176
- Goal: token-bucket limiter, 100 req/min/user, 429 + Retry-After
177
- Scope: src/middleware/rateLimit.ts (new), src/app.ts (+3 lines)
178
- Pipeline: implementer → tester → reviewer (correctness)
179
- Assumption: in-memory bucket, not Redis — say the word to change
180
- Boundary: src/legacy/** untouched
181
- Approve to proceed?
182
-
183
- you approved, go
184
-
185
- team ▸ @implementer — STATUS: done
186
- CHANGES: src/middleware/rateLimit.ts (new) · src/app.ts (+3)
187
- EVIDENCE: tsc clean
188
-
189
- ▸ @tester — STATUS: done · VERDICT: pass (14/14)
190
- FINDINGS: burst edge, window expiry, concurrent refill covered;
191
- src/legacy/** byte-identical
192
-
193
- ▸ @reviewer — STATUS: done · VERDICT: approve (correctness)
194
-
195
- team Done. rateLimit.ts (new) · app.ts (+3) · 14 tests green
196
- review: approve · assumption: in-memory only · src/legacy/ untouched
197
- ```
198
-
199
- You typed the task and four words. The plan was a routing-table lookup,
200
- execution waited for your approval, and every handoff between agents traveled
201
- as a structured skeleton — nothing landed in your repo, nothing was guessed.
202
-
203
- ---
204
-
205
- ## 🧰 Governed tools & security
206
-
207
- Every tool TeamMode adds runs under ONE governance pipeline: outputs above
208
- the offload threshold never enter the context window — the boundary is
209
- content-class aware (`TM_OFFLOAD_THRESHOLD_TEXT` = 4000 for prose:
210
- text/log/markdown; `TM_OFFLOAD_THRESHOLD_DATA` = 2000 for json/csv/code/
211
- binary; an unknown class falls back to the global `TM_OFFLOAD_THRESHOLD`) —
212
- and oversized results are offloaded to a run store and replaced by a
213
- content-aware preview plus an HMAC-signed handle that the agent pages
214
- through with `tm_fetch` when it genuinely needs the payload.
215
-
216
- | Tool | What it does | Roles |
217
- |---|---|---|
218
- | `tm_read` / `tm_grep` / `tm_bash` / `tm_fetch` | Governed file read / regex search / read-only shell (allowlist) / paged handle retrieval (JSON handles take a `fields` dot-path projection — a deliberately small jq subset like `items[].name`) | all six agents |
219
- | `tm_memory` | Session + project + global memory store (Markdown + frontmatter): add / search / list / forget / compact | all six agents |
220
- | `tm_ptc_run` | Batch orchestration: one program, N governed calls, zero LLM round-trips; web roles also get `tm.search` / `tm.webfetch` inside the program | all six agents |
221
- | `tm_search` | Multi-engine web search with extracted, deduplicated, RRF-fused hit lists | Lead + Researcher |
222
- | `tm_webfetch` | Single governed GET of an allowlisted page (search pages auto-extracted) | Lead + Researcher |
223
- | `tm_dispatch` / `tm_join` | **Async sub-agent dispatch**: `tm_dispatch` starts a specialist in its own child session and returns the id immediately (the built-in `task` tool blocks you until it finishes), `tm_join` collects — status snapshot, bounded `waitMs`, `cancel:true` to abort a runaway child; collected replies ride the offload pipeline, so five fat reports arrive as handles + previews instead of multiplying your context. Lead-only, and a child never dispatches. Each child is a real session the user can open from the session tree (titled `<description> (@<agent> subagent ·tm)`) and one toast names it — because the tool card itself is not expandable | Lead only |
224
- | `tm_pty` | **Non-blocking command execution** on the host's own terminal sessions (`start`/`status`/`list`/`kill`) — independent builds and test suites overlap instead of queueing behind one 120 s bash call. Captures no output (the command tees its own log; read it with `tm_read`), and every start passes the R6 classifier, the R2 danger-face globs **and** the official confirmation dialog before a process exists | Lead only |
225
- | `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), PTC internals, governance counts (blocked subresources, refused `tm_pty` starts, clamped bash 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 |
226
- | `tm_browser` | Interactive browser session (**your default browser**): 16 Playwright verbs (snapshot-first `take_snapshot` → uid-addressed `click`/`fill`/`drag`/…) + 5 legacy compat verbs (open/navigate/read/screenshot/close); Playwright engine needs Node ≥ 20, below that (or on any import failure) it auto-degrades to the legacy CDP engine. It drives YOUR default browser channel (an Edge Beta default opens Edge Beta), stays headful unless the operator sets `TM_BROWSER_HEADLESS`, loads a page's own images/CSS/JS via the `same-site` subresource policy, and `take_screenshot { image:true }` attaches a JPEG so the model can actually see the screen | Lead + Researcher + Tester (UI verification) |
227
-
228
- > **Fixed tool priority ladder (every task): ① the user's own MCP/plugin
229
- > tools → ② TeamMode governed tools (`tm_*`) → ③ the model's own reasoning.**
230
- > It doubles as the fallback chain: when a tool errors (no browser on this
231
- > host, blocked host), the agent says so and drops to the next rung — and
232
- > rung ③ is where a missing capability gets reported, never fabricated.
233
- > One stated exception: on the **web** channel `tm_search` / `tm_webfetch` /
234
- > `tm_browser` come first, because that path is the only one carrying the
235
- > domain allowlist, the per-request confirmation dialog and the R6 red lines.
236
-
237
- > **What the host UI cannot show you.** OpenCode renders a *plugin* tool call
238
- > as a one-line card with no expandable body — its tool-renderer registry
239
- > holds only the built-in tool names, and an extension cannot add to it. The
240
- > governed output still exists: anything over the offload boundary is written
241
- > to disk as a handle, and `tm_stats { recent: 20 }` prints the call-by-call
242
- > recap with those handles and file paths, which you can open. Ask your agent
243
- > for it — "what did that tool actually return?"
244
-
245
- All governed tools are **parallel-safe**: the host may run a batch of
246
- `tm_search` / `tm_webfetch` / `tm_fetch` calls concurrently — each call gets
247
- its own step id and its own payload, and nothing cross-contaminates
248
- (proven by the parallel test suite).
249
-
250
- ### Context governance: offload, handles, previews
251
-
252
- Large tool outputs are context cost's main driver — every step re-sends the
253
- whole window. So results above the content-class offload boundary (prose →
254
- `TM_OFFLOAD_THRESHOLD_TEXT`, structured data → `TM_OFFLOAD_THRESHOLD_DATA`,
255
- unknown → global `TM_OFFLOAD_THRESHOLD`) are written to a local run store
256
- (`<repo>/.git/opencode-team/`, never your working tree) and replaced by a
257
- handle with a content-aware preview: JSON keys / CSV header + shape / log
258
- ERROR×N stats / code signatures / binary metadata, hard-capped at 80 tokens.
259
- When the agent actually needs the payload, it pages through with `tm_fetch`
260
- using an HMAC-signed, run-scoped, expiring handle — and on a JSON handle it
261
- can ask for a `fields` dot-path projection instead (a deliberately small jq
262
- subset: `items[].name`, `[].stargazers_count`), so a big API dump narrows to
263
- just the values needed without the raw body ever entering the window.
264
- `tm_bash` only allows read-only commands (allowlist), and failures come back
265
- as structured errors instead of raw dumps.
266
-
267
- ### Session + project + global memory (tm_memory)
268
-
269
- Durable facts — build commands, environment quirks, architecture decisions,
270
- your conventions — live as human-editable Markdown with frontmatter, in
271
- THREE tiers:
272
-
273
- - **`session`**: this conversation's transients only — in-process, TTL-swept
274
- (`TM_MEMORY_SESSION_TTL_MIN`, default 240 min), invisible to other
275
- sessions; ephemeral unless `TM_MEMORY_SESSION_PERSIST=1` writes them under
276
- `memories/sessions/<sid>/`.
277
- - **`project`** (default): `<repo>/.git/opencode-team/memories/…` — per checkout, git-adjacent. Facts about THIS repo: build commands, environment quirks, architecture decisions.
278
- - **`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.
279
-
280
- Actions: `add` / `search` (deterministic keyword scoring) / `list` / `forget` /
281
- `compact`; 4000 chars per memory. `search` walks ALL tiers with **session >
282
- project > global precedence** — project entries get a +2 near-tie weight and a
283
- same-title higher-layer twin shadows the lower one (it never surfaces).
284
- Near-duplicates never pile up: an `add` that hits an existing entry in the same
285
- tier and category (dedup key, or title+keywords Jaccard ≥ 0.6) **folds into
286
- it** — new content wins, keywords union, the old slug moves to `supersedes:`,
287
- and the answer says 已合并 (that is normal; don't re-add under a variant title).
288
- When a tier reaches `TM_MEMORY_MAX_ENTRIES` (200 per scope) the add fails on
289
- purpose: run `compact` first — it dry-runs the merge plan by default, and
290
- re-running with `apply:true` performs it after copying every original to a
291
- timestamped `.compact-backup` tree (the rollback path). Entries older than
292
- `TM_MEMORY_STALE_DAYS` (30) surface tagged `[stale Nd]` in search. Agents
293
- are prompted to search before assuming conventions and to save hard-won
294
- facts for the next conversation.
295
-
296
- ### 🌐 Web search that actually works (in China)
297
-
298
- `tm_search` is the open-ended-lookup front: **one call, one query, clean
299
- results**. The engine URL is built for you, fetched through the governed
300
- pipeline, and collapsed into a numbered title+URL hit list — the agent never
301
- sees raw SERP chrome.
302
-
303
- | Engine | Notes |
304
- |---|---|
305
- | `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` |
306
- | `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 |
307
- | `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 |
308
- | `hn` | Hacker News via Algolia API (no key) → story titles + snippets with direct article URLs |
309
- | `bilibili` | video search |
310
- | `moegirl` | MediaWiki search API — entry titles + snippets, structured |
311
- | `npm` | registry search → name@version + description, structured |
312
- | `github` | repo search API → stars + description, structured; `org:` / `user:` / `stars:` / `language:` qualifiers fold into the query (e.g. `vector db stars:>500 language:rust`) |
313
-
314
- All seven engines are reachable from mainland China **without API keys**, and
315
- every one of them sits on the seeded domain allowlist. The previous CN HTML
316
- SERPs (`sogou` / `so` / `baidu` / `bing-int`) were **removed** — a live
317
- benchmark (2026-09-14) showed them serving anti-bot shells or 100% empty
318
- results; they are not even manually selectable. On an empty result,
319
- the error names the alternative engines instead of leaving the agent stuck.
320
- Two more channels complete the surface:
321
-
322
- - `tm_webfetch` — a known URL, one governed GET. Search-engine pages it
323
- fetches are auto-extracted to hit lists too. JSON endpoints like
324
- `registry.npmjs.org/<pkg>/latest` pass through untouched.
325
- - `tm_browser` — JS-rendered pages: **your DEFAULT browser** (Windows
326
- registry / Linux `xdg-settings`; Chromium-family only — Firefox falls back
327
- to the Edge/Chrome probe order because CDP is Chromium-proprietary;
328
- `TM_BROWSER_PATH` overrides), headful by default, isolated temp profile,
329
- **domain allowlist enforced at the network layer** per request and per
330
- redirect hop. The action surface is chrome-devtools-mcp aligned: **16
331
- Playwright verbs** (`navigate_page` · `take_snapshot` · `click` · `fill` ·
332
- `hover` · `drag` · `press_key` · `select_page` · `upload_file` · `wait_for`
333
- · `evaluate_script` · `list_console_messages` · `list_network_requests` ·
334
- `list_pages` · `take_screenshot` · `handle_dialog`) plus 5 legacy compat
335
- verbs (`open` / `navigate` / `read` / `screenshot` / `close`).
336
- Snapshot-first: `take_snapshot` returns the aria snapshot with injected
337
- `[uid=eN]` tokens, follow-up actions address nodes by uid instead of
338
- guessed locators; snapshots are hard-capped by
339
- `TM_BROWSER_SNAPSHOT_MAX_TOKENS` (default 1200).
340
- **Engine split:** the primary engine is `playwright-core` (an optional
341
- dependency — needs **Node ≥ 20**; on older Node, or if the import fails,
342
- the session auto-degrades per instance to the zero-dep `cdp-legacy`
343
- engine, which keeps the core verbs only; pin either via
344
- `TM_BROWSER_ENGINE=playwright|cdp-legacy`). No browser download is ever
345
- involved — Playwright launches YOUR installed browser by path, so
346
- `npx playwright install` is not part of the user flow (the dependency
347
- itself resolves at npm install/publish time). Isolated temp profile by
348
- default: persistent logins only if you explicitly set
349
- `TM_BROWSER_USER_DATA_DIR`.
350
-
351
- When a fetch still returns **403 after the real-Chrome headers**, the error
352
- is a DIRECTIVE: the gate is JS-challenge / TLS-fingerprint based and only a
353
- real browser passes — the agent is told to call `tm_browser`
354
- (`action:"open"` → `action:"read"`) for that URL. Search hit lists also
355
- filter known noise: engine-internal wrappers (`so.com/link?`, `ai.so.com`)
356
- and same-name-different-site domains (`maimai.cn` 脉脉 vs the maimai DX
357
- game) never ride along — extend the hit blacklist with `TM_HIT_BLACKLIST`.
358
-
359
- Seeded allowlist (all three web tools; 23 hosts — baidu/moegirl/bilibili are
360
- PARENT domains, so every sibling subdomain — baike.baidu.com,
361
- mzh.moegirl.org.cn, space.bilibili.com — is covered):
362
- `baidu.com`, `moegirl.org.cn`, `bilibili.com`, `www.sogou.com`, `www.so.com`,
363
- `cn.bing.com`, `www.bing.com`, `zhihu.com`, `juejin.cn`, `csdn.net`,
364
- `cnblogs.com`, `gitee.com`, `github.com`, `api.github.com`,
365
- `raw.githubusercontent.com`, `gist.githubusercontent.com`, `ghproxy.net`
366
- (mainland mirror for github raw), `stackoverflow.com`, `api.stackexchange.com`
367
- + `hn.algolia.com` (the two JSON search engines), `npmjs.org`, `pypi.org`,
368
- `learn.microsoft.com` — extend via
369
- `TM_WEBFETCH_ALLOWED_DOMAINS` (`"*"` opens every host; a custom list REPLACES
370
- the seed, so keep the engine hosts or `tm_search` loses its targets). Architect /
371
- implementer / reviewer have NO network grant — web questions come back as a
372
- reported gap, never simulated. The tester carries `tm_browser` ONLY, for
373
- governed UI verification of the project (local dev servers, preview routes);
374
- open web fetching stays with the two network roles.
375
-
376
- **Out-of-allowlist targets are a gate, not a wall.** When a fetch / search /
377
- browser-open points at a host outside the allowlist, the tool hands the URL
378
- to OpenCode's **official confirmation dialog** — you decide, once per
379
- target (an unanswered dialog is auto-rejected on the usual 1-minute timer,
380
- and the plugin still never self-allows). Every dialog also fires a
381
- **system toast notification**, so you know something is waiting even when
382
- you're not staring at the screen. Env-file URLs and non-http(s) schemes
383
- remain hard-rejected with no dialog — R6 red lines are never consentable.
384
-
385
- ### Security: the R6 + R2 approval gate
386
-
387
- **R6 environment protection.** With TeamMode active, the model cannot read
388
- environment variables silently. Env reads (`printenv`, `env`, `Get-ChildItem
389
- env:`, …) and env files (`.env`, shell rc) route through OpenCode's official
390
- confirmation dialog; unanswered prompts are **auto-rejected after
391
- `TM_ASK_TIMEOUT_MIN` (default 1 min)**. Env reads no wildcard can express
392
- (embedded `$VAR` / `${VAR}` / `$env:` inside another command, command
393
- substitution) and the `tm_*` wrapper channel stay a **hard block** — no dialog
394
- to slip through. The audit log records only tool name + pattern category +
395
- verdict — never command text, paths, variable names or values. Verdicts now
396
- also explain a dialog that cannot be answered anymore: a reply that hits an
397
- **already-closed** dialog (host 404 — the timer already auto-rejected) audits
398
- as `already-closed` without degrading the gate, a plugin-side bad reply shape
399
- audits as `rejected-shape-bug`, and a user reply that arrives *after* the
400
- auto-reject is recorded as `late-<verdict>` for observability only (the
401
- rejection stands — the plugin still never self-allows). The 1-minute auto-reject
402
- floor is tunable via `TM_ASK_TIMEOUT_FLOOR_MIN`; the short default is safe
403
- because a reply that races the timer audits as benign `already-closed` (host
404
- 404 on a closed dialog — no gate degrade), and the observed ~120 s is the
405
- host-to-plugin event-bus *delivery* lag, not click-resolution latency.
406
-
407
- **R2 dangerous operations (same dialog).** Delete, git publish, network
408
- fetch, package install/publish, process/system, privilege changes — none are
409
- silently allowed. The normal verification stack (`npm test`, `tsc`,
410
- `git status`) is NOT gated, so day-to-day work runs uninterrupted.
411
-
412
- > ⚠️ **When you approve a dialog, pick "once" — not "always".** Verified on
413
- > the live host, "always" records a far broader rule than the command you
414
- > saw: approving `Get-ChildItem env:PATH` with "always" stores `Get-ChildItem
415
- > *`, so every later `Get-ChildItem` runs with no dialog at all.
416
-
417
- > Deferral to the dialog is per-session: env reads only route to the dialog
418
- > in sessions running TeamMode's injected agents. In any other session the
419
- > guard keeps hard-blocking env reads. `headless opencode run` auto-rejects
420
- > unanswered `ask`s immediately (there is no human to prompt).
421
-
422
- ### Repo hygiene
423
-
424
- The offload/trajectory/memory stores live under `<repo>/.git/opencode-team/`
425
- (or the OS temp dir outside a git repo) — **never your working tree**. Every
426
- agent is instructed to delete scratch files before reporting done and to keep
427
- throwaway work in the OS temp dir. A TTL sweeper reclaims old task dirs at
428
- startup + hourly; the Team Lead never deletes boards itself, so you can audit
429
- any run.
430
-
431
- ---
432
-
433
- ## ⚙️ Configuration
434
-
435
- The plugin injects everything at startup — no agent files to copy.
436
-
437
- > **Model choice matters.** Every judgment — triage, decomposition, dispatch
438
- > briefs, synthesis, review verdicts — flows through the Team Lead. A weak
439
- > model in that seat degrades the whole pipeline no matter how strong the
440
- > specialists are. Pin your best reasoning model to `team`:
441
-
442
- ```jsonc
443
- {
444
- "agent": {
445
- "team": { "model": "anthropic/claude-opus-4-5" }, // Lead earns your best model
446
- "implementer": { "model": "anthropic/claude-sonnet-4-6" } // specialists tolerate cheaper
447
- }
448
- }
449
- ```
450
-
451
- **Global install** — put the plugin entry in `~/.config/opencode/opencode.jsonc` and every project gets the team.
452
-
453
- **Keep Team off the default slot:**
454
-
455
- ```jsonc
456
- {
457
- "plugin": [
458
- ["@te-river/opencode-team-mode@latest", { "defaultAgent": false }]
459
- ]
460
- }
461
- ```
462
-
463
- Your own agents named `team` / `architect` / … always take precedence; the
464
- plugin never clobbers user definitions. See [Customization](#-customization)
465
- for overrides, extra agents and disabling roles.
466
-
467
- ### Environment variables
468
-
469
- | Env var | Default | Purpose |
470
- |---|---|---|
471
- | `TM_ENV_PROTECT` | `strict` | R6 mode: `strict` / `standard` / `off` (off also disarms the approval timer) |
472
- | `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) |
473
- | `TM_ASK_TIMEOUT_FLOOR_MIN` | `1` | minimum enforced for the ask timeout above |
474
- | `TM_ENV_PROTECT_EXTRA_DENY` | — | extra block patterns (regex; always hard block, never dialog-governed) |
475
- | `TM_OFFLOAD_THRESHOLD` | `2000` | global offload fallback (tokens, CJK-aware estimate) — used when the content class is unknown |
476
- | `TM_OFFLOAD_THRESHOLD_TEXT` | `4000` | offload boundary for prose (text / log / markdown) |
477
- | `TM_OFFLOAD_THRESHOLD_DATA` | `2000` | offload boundary for structured payloads (json / csv / code / binary) |
478
- | `TM_PREVIEW_MAX_TOKENS` | `80` | preview hard cap |
479
- | `TM_FETCH_MAX_LINES` | `2000` | tm_fetch page cap |
480
- | `TM_BLACKBOARD_DIR` / `TM_TRAJECTORY_DIR` | `<repo>/.git/opencode-team/…` | offload store / trajectory ledger (tmpdir fallback; explicit = absolute or project-relative) |
481
- | `TM_BLACKBOARD_TTL` | `7` | store retention (days) |
482
- | `TM_BASH_READONLY_ALLOWED` | built-in table | tm_bash allowlist |
483
- | `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) |
484
- | `TM_WEBFETCH_ALLOWED_DOMAINS` | the 23 seeded hosts | tm_webfetch / tm_search / tm_browser allowlist (`"*"` opens all; empty = deny all; a custom list REPLACES the seed — keep the engine hosts) |
485
- | `TM_BROWSER_PATH` | auto-detect | tm_browser executable override (default: your DEFAULT browser when Chromium-family, else Edge/Chrome probes) |
486
- | `TM_BROWSER_HEADLESS` | `auto` | `1` headless (CI) / `0` headful / `auto` (headless only on display-less Linux) |
487
- | `TM_BROWSER_ENGINE` | `playwright` | `playwright` (needs Node ≥ 20; any import failure auto-degrades) / `cdp-legacy` (zero-dep CDP pipe, core verbs only) |
488
- | `TM_BROWSER_SNAPSHOT_MAX_TOKENS` | `1200` | hard cap on `take_snapshot` payloads |
489
- | `TM_BROWSER_SUBRESOURCE` | `same-site` | what a page may load after its navigation was allowed: `same-site` = images/media/fonts/stylesheets always, scripts/XHR only for a site this session actually opened; `passive` = only the passive types; `off` = the legacy every-request gate. Blocked requests surface as a "N 个子资源请求被拦截" note on the next snapshot |
490
- | `TM_BROWSER_IDLE_MS` | `180000` | an untouched browser session closes itself after this many ms (0 disables) and tells the user — a window nobody owns is a user-facing bug |
491
- | `TM_BROWSER_ASK_EVAL` | `on` | `evaluate_script` runs arbitrary JS in YOUR browser — the one verb the domain allowlist can't cover (it limits where we navigate, not what a loaded page hands back). One official-dialog consent per browser session; no ask bridge ⇒ refused. `off` restores the old behaviour; the result redaction (JWT/bearer/cookie/api-key shapes) is NOT switchable |
492
- | `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 / the PTC bridge; 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 缓存命中 |
493
- | `TM_BROWSER_IMAGE_MAX_BYTES` | `400000` | ceiling on the JPEG `take_screenshot { image:true }` inlines into the model's context (above it the reply stays path-only and says why) |
494
- | `TM_SEARCH_WEIGHTS` | unset | per-engine fusion weight overrides, e.g. `bing=0.3,hn=0.25`; anything unset keeps the built-in table |
495
- | `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) |
496
- | `TM_SEARCH_MAX_HITS` | `10` | hits kept per engine leg and in the fused list |
497
- | `TM_SEARCH_DISABLED_ENGINES` | unset | engines removed from the roster AND from every `auto` route (`sogou,baidu` style) |
498
- | `TM_BASH_TIMEOUT_PROBE_MS` | `60000` | ceiling forced onto a `timeout` the model set for a read-only probe command (0 disables) |
499
- | `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 |
500
- | `TM_PTY_MAX` | `4` | concurrent `tm_pty` terminal sessions this plugin may keep running at once |
501
- | `TM_DISPATCH_ASK` | `on` | `tm_dispatch` opens the official confirmation dialog before spawning a sub-agent — the same gate the built-in `task` tool applies (`ctx.ask` per sub-agent type), so a plugin-side dispatcher is not a way around the user's rules. No ask bridge ⇒ refused. `off` skips it (the lead-only lock still applies) |
502
- | `TM_SUBAGENT_DEPTH` | `1` | nesting ceiling for dispatched children, same 口径 as the host's `subagent_depth` (1 = sub-agents never spawn sub-agents). Enforced plugin-side because that check lives in the task tool, not in the session API |
503
- | `TM_PARALLEL_DISPATCH` | `on` | whether the lead may have more than one sub-agent running at a time. `off` refuses a second `tm_dispatch` while any child is still live (the refusal names the running children, and happens before the confirmation dialog), so the team works serially: dispatch → `tm_join` → next. For a machine or provider quota that cannot carry N sessions at once |
504
- | `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) |
505
- | `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 |
506
- | `TM_COMPACTION_CONTEXT` | `on` | on the host's pre-compaction hook, add the must-survive list (reply skeleton, offload handles, dispatched child session ids, provenance, board paths). Additive — the host's own summarizer prompt is never replaced |
507
- | `TM_COMPACTION_AUTOCONTINUE` | `on` | `off` stops the host from silently resuming the turn after a compaction, so a human re-reads state first |
508
- | `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 |
509
- | `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 |
510
- | `TM_BROWSER_USER_DATA_DIR` | — (isolated temp profile) | explicit persistent profile dir — the ONLY way logins survive between sessions |
511
- | `TM_MEMORY_GLOBAL_DIR` | `~/.opencode-team/memories/global/` | tm_memory GLOBAL tier store |
512
- | `TM_MEMORY_SESSION_TTL_MIN` | `240` | session-tier entry TTL (lazy + boot sweep) |
513
- | `TM_MEMORY_MAX_ENTRIES` | `200` | per-scope entry cap; over it `add` fails on purpose — run `compact` |
514
- | `TM_MEMORY_STALE_DAYS` | `30` | age after which search hits are tagged `[stale Nd]` (`0` disables) |
515
- | `TM_MEMORY_SESSION_PERSIST` | — (ephemeral) | `1` also writes session entries under `memories/sessions/<sid>/` |
516
- | `TM_HIT_BLACKLIST` | `maimai.cn` | extra domains never listed as search hits (comma/semicolon separated; same-name-different-site noise like 脉脉) |
517
- | `TM_PTC_MAX_PROGRAM_CHARS` | `4000` | PTC program source cap |
518
- | `TM_PTC_MAX_CALLS` | `20` | PTC per-run bridge-call budget (1–200) |
519
- | `TM_PTC_MAX_ERRORS` | `3` | PTC per-run error budget (1–50) |
520
- | `TM_PTC_TIMEOUT_MS` | `60000` | PTC per-run wall-clock timeout (5s–10min) |
521
- | `TM_PTC_ENGINE` | `auto` | `auto` (worker→inline degrade) / `worker` / `inline` |
522
- | `TM_PTC_WEB_BRIDGE` | `on` | expose `tm.search` / `tm.webfetch` to PTC programs (`off` removes them from the bridge set) |
523
-
524
- ---
525
-
526
- ## 🏗️ How the team works
527
-
528
- Sub-agents can't message each other live (platform limitation), so TeamMode
529
- coordinates them through a **structured reply skeleton** — every specialist
530
- reply is `STATUS: / CHANGES: / FINDINGS: / EVIDENCE: / HANDOFF:`, ≤50 lines,
531
- relayed verbatim by the Lead into the next dispatch. Files are the exception,
532
- not the rule: a deliverable over ~50 lines goes to ONE named board file under
533
- `<repo>/.git/opencode-team/` (round-suffixed, working tree untouched).
534
-
535
- - **Routing table:** question → direct answer; docs-only → implementer;
536
- product change → implementer → tester → reviewer; multi-module →
537
- architect → implementer → tester → reviewer(s); unknown tech → researcher
538
- first. Fixed minimums — a product change routed below 3 dispatches is a
539
- routing bug.
540
- - **Approval gate (count-based):** ≥2 dispatches → plan (≤30 lines) → **your
541
- approval** → execute. Blocking questions are batched and asked immediately.
542
- - **Concurrent dispatch:** independent dispatches batch into the SAME round
543
- (parallel implementers with per-file ownership + verbatim contracts,
544
- 3-dimension reviews, split test suites); only the Team Lead dispatches —
545
- specialists no longer hold `task`, so no sub-agent spawns a sub-agent.
546
- - **Adaptive review:** one reviewer by default; three parallel dimensions
547
- only for high-risk profiles (auth/security, cross-module contracts, public APIs).
548
- - **Static verification:** build / typecheck / lint / tests. Improvised
549
- browser automation is banned; unverified UI work ends with
550
- `UI NOT VERIFIED: <what to check>`.
551
- - **Evidence standard:** "done / fixed / passed" claims need verifiable
552
- evidence — output, logs, diffs.
553
-
554
- ---
555
-
556
- ## 🔧 Customization
557
-
558
- **Override an agent** — same name in your config wins:
559
-
560
- ```jsonc
561
- {
562
- "agent": {
563
- "reviewer": {
564
- "model": "anthropic/claude-sonnet-4-6",
565
- "prompt": "You are an extremely strict reviewer. Reject anything with a lint warning."
566
- }
567
- }
568
- }
569
- ```
570
-
571
- **Add your own agents** alongside the team:
572
-
573
- ```jsonc
574
- {
575
- "agent": {
576
- "devops": {
577
- "mode": "subagent",
578
- "description": "Handles CI/CD, Docker, and deployment tasks.",
579
- "prompt": "You are the DevOps engineer..."
580
- }
581
- }
582
- }
583
- ```
584
-
585
- **Disable one:** `"researcher": { "disable": true }`.
586
-
587
- **Board retention** via the tuple form: `["@te-river/opencode-team-mode@latest", { "ttlDays": 7 }]` (valid range (0, 365], invalid values fall back to 5).
588
-
589
- ---
590
-
591
- ## ❓ FAQ
592
-
593
- **Will this eat my tokens?**
594
- The opposite is the point. Offload + 80-token previews + PTC batch programs
595
- exist because a five-agent pipeline naively bolted onto one context window
596
- *would* eat your tokens. The governance is the token-saver.
597
-
598
- **Is the web access safe?**
599
- It's the most guarded surface in the plugin: two full web roles plus a
600
- browser-only tester grant, a domain
601
- allowlist with dialog-gated escapes (you approve any out-of-allowlist
602
- target in OpenCode's official dialog, with a toast notification), redirects
603
- re-checked per hop, network-layer enforcement in the browser, env-file URL
604
- refusal, and every payload rides the same offload governance. No allowlisted
605
- page can bounce the fetch off-site.
606
-
607
- **Why doesn't the plugin auto-update?**
608
- OpenCode caches plugins by spec string and never re-resolves `@latest`
609
- (upstream limitation, not ours). **Re-run the installer — that IS the
610
- update** (it purges the cache and re-resolves npm copies); or delete the
611
- cache dir by hand. Recipe above and in the
612
- [installation guide](./docs/installation.md).
613
-
614
- **Can agents run tools in parallel?**
615
- Yes — and they're *engineered* for it: parallel `tm_search` / `tm_webfetch` /
616
- `tm_fetch` calls get distinct step ids and isolated payloads. A regression
617
- here fails the test suite before it ever reaches you.
618
-
619
- **Does it work in the CLI (TUI), or only Desktop?**
620
- Both. Desktop adds the color-coded picker and panels; the governed tools and
621
- the whole workflow are host-agnostic. On display-less Linux, `tm_browser`
622
- runs headless automatically.
623
-
624
- **What happens if I don't answer a confirmation dialog?**
625
- It auto-rejects after `TM_ASK_TIMEOUT_MIN` (default 1). The plugin never
626
- self-approves — the only side it can take is yours or nobody's.
627
-
628
- ---
629
-
630
- ## 🗑️ Uninstall
631
-
632
- 1. Remove the entry from the `"plugin"` array in your config file.
633
- 2. Delete the cache dir (table in [Install](#-read-this-once-save-yourself-an-hour-later)) if you want the disk space back.
634
- 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.
635
-
636
- No DLLs were harmed. Nothing was written to your working tree.
637
-
638
- ---
639
-
640
- ## 🏛️ Architecture (for the curious)
641
-
642
- ```
643
- opencode-team-mode/
644
- ├── src/
645
- │ ├── index.ts ← Plugin entry (config + R6 guard + approval gate + tool segment)
646
- │ ├── agents.ts ← Agent structure (modes, colors, temperatures, whitelist matrix)
647
- │ ├── prompts/ ← Agent prompts (lead / specialists / shared) — pinned by tests
648
- │ ├── commands.ts ← Slash command definitions
649
- │ ├── blackboard.ts ← Shared blackboard + TTL sweeper
650
- │ ├── envprotect.ts ← R6 facade (patterns / classifiers / gate predicates / hook)
651
- │ ├── approval-gate.ts ← Unified approval gate (dialog timeout auto-reject)
652
- │ ├── tm/ ← Governed tools: pipelines / store / preview / guard / refs /
653
- │ │ webfetch / search / memory / browser / shell-bridge / ptc/ (9 modules)
654
- │ └── types.ts ← Loader-contract types (1.18.x)
655
- ├── docs/installation.md ← The agent-consumable install guide
656
- ├── scripts/ ← One-line installers (bash / PowerShell)
657
- ├── pt07/ ← PT-07 baseline suite (seeded A/B token measurement)
658
- └── README.*.md ← You are here (twice)
659
- ```
660
-
661
- The loader calls `server(input, options)` once: the `config` hook injects the
662
- six agents and six commands, the same call installs the R6
663
- `tool.execute.before` guard, arms the approval gate through an `event` hook,
664
- and registers the `tm_*` tools. User-defined agents with the same name always
665
- win — the plugin never clobbers.
666
-
667
- ---
668
-
669
- ## 🤝 Contributing
670
-
671
- Issues and PRs welcome. Especially wanted: localization of agent prompts,
672
- more agent roles, more command templates, and real-world reports of the
673
- search engines' behavior (they rearrange their markup; the extractor filters
674
- are intentionally loose but not psychic).
675
-
676
- ---
677
-
678
- ## 📄 License
679
-
680
- [Apache License 2.0](./LICENSE)
681
-
682
- ---
683
-
684
- ## 🔗 Links
685
-
686
- - [npm Package](https://www.npmjs.com/package/@te-river/opencode-team-mode) — `@te-river/opencode-team-mode`
687
- - [Installation guide](./docs/installation.md) — the complete manual / agent-consumable procedure
688
- - [OpenCode Desktop](https://opencode.ai) — Official website & download
689
- - [OpenCode Docs](https://opencode.ai/docs) — Configuration & plugin documentation
690
- - [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, 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. First run `opencode --version`:
23
+ > 1.18.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation.md
24
+ > 2.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation-v2.md
25
+ > (on 2.x a 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** | `tm_ptc_run`: 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 `tm_ptc_run`, 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 `tm_browser` |
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. First run `opencode --version`:
94
+ 1.18.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation.md
95
+ 2.x → https://raw.githubusercontent.com/Te-River/Opencode-TeamMode/main/docs/installation-v2.md
96
+ (on 2.x a 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_read` / `tm_grep` / `tm_bash` / `tm_fetch` | Governed file read / regex search / read-only shell (allowlist) / paged handle retrieval (JSON handles take a `fields` dot-path projection — a deliberately small jq subset like `items[].name`) | all six agents — **except on OpenCode 2.x, where the first three are not registered; see the note under this table** |
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_ptc_run` | Batch orchestration: one program, N governed calls, zero LLM round-trips; web roles also get `tm.search` / `tm.webfetch` inside the program | all six agents |
280
+ | `tm_search` | Multi-engine web search with extracted, deduplicated, RRF-fused hit lists | Lead + Researcher |
281
+ | `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 |
282
+ | `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 on OpenCode 2.x, which 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. **v2 only** — v1 keeps the host's `todowrite` | Lead only |
283
+ | `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 abort, 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 | Lead only |
284
+ | `tm_pty` (**v1 only** — v2 registers nothing: the plugin ctx has no pty domain) | **Non-blocking command execution** on the host's own terminal sessions (`start`/`status`/`list`/`kill`) — independent builds and test suites overlap instead of queueing behind one 120 s bash call. Captures no output (the command tees its own log; read it with `tm_read`), and every start passes the R6 classifier, the R2 danger-face globs **and** the official confirmation dialog before a process exists | Lead only |
285
+ | `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), PTC internals, governance counts (blocked subresources, refused `tm_pty` starts, clamped bash 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 |
286
+ | `tm_browser` | Interactive browser session (**your default browser**): 18 Playwright verbs (snapshot-first `take_snapshot` → uid-addressed `click`/`fill`/`drag`/…, **including multi-tab `new_page` / `close_page`** so you can hold two pages at once) + 5 legacy compat verbs (open/navigate/read/screenshot/close); Playwright engine needs Node ≥ 20, below that (or on any import failure) it auto-degrades to the legacy CDP engine. It drives YOUR default browser channel (an Edge Beta default opens Edge Beta), stays headful unless the operator sets `TM_BROWSER_HEADLESS`, loads a page's own images/CSS/JS via the `same-site` subresource policy, and `take_screenshot { image:true }` attaches a JPEG so the model can actually see the screen. **One browser per agent**: `open` returns an id (`b1`) and every reply is tagged with it — that window, its uid numbering and its dialog-approved hosts belong to YOUR session; another agent's id is refused with the owner named (an id is a name, not a key), and `close { id:"all" }` closes only your own. `click` reports what the PAGE did, not merely that a mouse event was sent: it reads the target's observable state before and after (`aria-expanded`, URL, DOM node count) and answers `已点击 … · aria-expanded: false → true`, retries once if the page had not finished loading — the measured cause of a click that lands on a node with no handler — and says 页面没有任何可观测变化 rather than implying success. `close` will not say 已确认关闭 until the browser's OS process is gone — it waits on the pid, terminates it once if it lingers, and prints the pid either way, because a dropped connection is not a closed browser; when no pid could be resolved it says 进程未核验 rather than borrowing that sentence. A browser this process launched but could not reap is recorded in a per-workspace ledger (pid, owner pid, executable) and reclaimed by the next boot — only entries whose owner process is dead *and* whose pid still is that executable, terminated as a whole tree (`taskkill /T` on Windows), never another window's live tab. **A blank page now explains itself**: `same-site` cannot know that a site's own bundle lives on a brand-unrelated CDN (Baidu serves its scripts from `bdimg.com`), so when a page comes back with `0 个可寻址节点` while a script host was blocked, the reply says the blankness is **our gate**, names the host, and offers `allow_host { host }` — one official dialog for one bare domain, for your browser only, for this session, nothing written to config (re-navigate afterwards, the gate decides per request). A page that is really a human-verification wall (百度安全验证 / Cloudflare / access denied) is reported as a wall, because the move there is another source, not another retry | Lead + Researcher + Tester (UI verification) |
287
+
288
+ > **On OpenCode 2.x, five of those tools are deliberately not registered.**
289
+ > `tm_read` / `tm_grep` / `tm_bash` are replaced by the host's own `read` / `grep` /
290
+ > `glob` / `shell`, and `tm_ptc_run` by the host's `execute` (Code Mode) — the same
291
+ > job, one fewer tool for the model to choose between. The governance did NOT
292
+ > disappear with them: an oversized native result is offloaded through
293
+ > `tool.execute.after` (measured: `shell` 12,902 tokens arriving as a 78-token
294
+ > preview), and the address red line plus R6's per-command classification ride the
295
+ > host's `permission.evaluate`. `tm_ledger` is the one tool that exists ONLY on v2 (the lead's list needs a home where
296
+ > `todowrite` does not).
297
+ > **Nothing we change reaches outside Team.** Every v2 hook fires for every session on
298
+ > the host, so the tool-surface trim, the 0.2 temperature, the blackboard note, the offload
299
+ > of native results, the R6/address strictening and the forced-background dispatch all ask
300
+ > "is this one of our six roles?" first — `build`, `plan` and your own agents stay as a
301
+ > fresh install leaves them. Calls whose owner the host did not tell us are left alone too,
302
+ > and counted (`tm_stats` prints `作用域:我们 · 他人 · 未判定`).
303
+ > The installer writes the v2 variants of the six
304
+ > role prompts, which name `read` / `grep` / `shell` instead of the aliases.
305
+ > Everything above this note describes the v1 (1.18.x) surface, which still ships
306
+ > all thirteen.
307
+
308
+ > **On OpenCode 2.x, interactive browsing is the host's, and it stays ours.** The
309
+ > desktop renders a browser in its side panel, and that panel attaches to the
310
+ > server's own browser service — a plugin cannot register a page of its own into it
311
+ > (forensics: `docs/research/browser-pane.md`). So the three roles with network
312
+ > grants (lead, researcher, tester) are pointed at the host's `browser_*` tools
313
+ > first, and `tm_browser` remains the governed door for hosts with no desktop
314
+ > browser attached (CLI, standalone). Handing browsing over did not mean handing
315
+ > governance over: `permission.evaluate` was observed NOT firing for `browser_*`, so
316
+ > our gate sits on `tool.execute.before` instead — it classifies the URL of every
317
+ > navigate/open, the path of every `browser_preview`, and every browser URL named
318
+ > inside an `execute` program, refuses the ones out of policy, and if the host runs a
319
+ > refused call anyway it replaces the page with the same refusal, so no
320
+ > out-of-policy content reaches the context, the store or the trajectory. What we
321
+ > cannot do is un-make a request the host already performed, and the reply says so
322
+ > rather than claiming a block; `tm_stats` prints the two numbers apart (refused /
323
+ > leaked past the refusal). One exception to the offload rule lives here: a
324
+ > `browser_snapshot` is an addressing table, not a document, so it is CAPPED (every
325
+ > `[ref=…]` line kept, static text dropped, budget `TM_NATIVE_SNAPSHOT_MAX_TOKENS`
326
+ > default 1 200 — the same budget `tm_browser` runs at) rather than replaced by a
327
+ > handle. Measured on a 261-ref page: a head cut keeps 118 refs, this keeps 261 of
328
+ > them using 1 044 of 11 326 tokens.
329
+
330
+ > **Fixed tool priority ladder (every task): ① the user's own MCP/plugin
331
+ > tools → ② TeamMode governed tools (`tm_*`) → ③ the model's own reasoning.**
332
+ > It doubles as the fallback chain: when a tool errors (no browser on this
333
+ > host, blocked host), the agent says so and drops to the next rung — and
334
+ > rung ③ is where a missing capability gets reported, never fabricated.
335
+ > One stated exception: on the **web** channel `tm_search` / `tm_webfetch` /
336
+ > `tm_browser` come first, because that path is the only one carrying the
337
+ > domain allowlist, the per-request confirmation dialog and the R6 red lines.
338
+
339
+ > **What the host UI cannot show you.** OpenCode renders a *plugin* tool call
340
+ > as a one-line card with no expandable body — its tool-renderer registry
341
+ > holds only the built-in tool names, and an extension cannot add to it. The
342
+ > governed output still exists: anything over the offload boundary is written
343
+ > to disk as a handle, and `tm_stats { recent: 20 }` prints the call-by-call
344
+ > recap with those handles and file paths, which you can open. Ask your agent
345
+ > for it — "what did that tool actually return?"
346
+
347
+ > **Delegation goes through the host's `task` — one channel, two shapes.**
348
+ > OpenCode's own `task` tool is the only sub-agent the interface can SHOW you:
349
+ > its card links to the live child session, and the user can stop it. Add
350
+ > `background: true` and it is also non-blocking — the host wakes your lead when
351
+ > the child finishes. That flag is experimental, so you turn it on yourself:
352
+ >
353
+ > ```
354
+ > OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
355
+ > ```
356
+ >
357
+ > (set it for the app process — `setx` on Windows, or launch from a shell that
358
+ > exports it — then restart OpenCode; the installers do it for you, and that is
359
+ > a **persistent user-scope write**, so see
360
+ > [What the installer changes on your machine](#-what-the-installer-changes-on-your-machine)
361
+ > for the exact value, its lifetime and the undo command). TeamMode
362
+ > then keeps that channel inside your token budget: the injected full reply is
363
+ > replaced by a preview plus a pointer, and nothing is copied to disk
364
+ > (`TM_TASK_OFFLOAD=off` restores the host's verbatim text). Cost of the host
365
+ > path, stated plainly: each finished background task wakes the lead and costs a
366
+ > turn. `tm_join` is the read side that stays — it pulls a named child's whole
367
+ > reply back through the offload pipeline, aborts a runaway, and rebuilds its
368
+ > registry from the host session tree after a restart.
369
+ >
370
+ > There is deliberately **no plugin-side dispatcher**: an earlier `tm_dispatch`
371
+ > created children the user could neither open from a card nor stop from the
372
+ > interface, which is a worse trade than the batching it bought. Remove it,
373
+ > and nothing breaks — the host's `task` was always the visible path.
374
+
375
+ All governed tools are **parallel-safe**: the host may run a batch of
376
+ `tm_search` / `tm_webfetch` / `tm_fetch` calls concurrently — each call gets
377
+ its own step id and its own payload, and nothing cross-contaminates
378
+ (proven by the parallel test suite).
379
+
380
+ ### Context governance: offload, handles, previews
381
+
382
+ Large tool outputs are context cost's main driver — every step re-sends the
383
+ whole window. So results above the content-class offload boundary (prose →
384
+ `TM_OFFLOAD_THRESHOLD_TEXT`, structured data → `TM_OFFLOAD_THRESHOLD_DATA`,
385
+ unknown → global `TM_OFFLOAD_THRESHOLD`) are written to a local run store
386
+ (`<repo>/.git/opencode-team/`, never your working tree) and replaced by a
387
+ handle with a content-aware preview: JSON keys / CSV header + shape / log
388
+ ERROR×N stats / code signatures / binary metadata, hard-capped at 80 tokens.
389
+ When the agent actually needs the payload, it pages through with `tm_fetch`
390
+ using an HMAC-signed, run-scoped, expiring handle — and on a JSON handle it
391
+ can ask for a `fields` dot-path projection instead (a deliberately small jq
392
+ subset: `items[].name`, `[].stargazers_count`), so a big API dump narrows to
393
+ just the values needed without the raw body ever entering the window.
394
+ `tm_bash` only allows read-only commands (allowlist), and failures come back
395
+ as structured errors instead of raw dumps.
396
+
397
+ ### Session + project + global memory (tm_memory)
398
+
399
+ Durable facts — build commands, environment quirks, architecture decisions,
400
+ your conventions — live as human-editable Markdown with frontmatter, in
401
+ THREE tiers:
402
+
403
+ - **`session`**: this conversation's transients only — in-process, TTL-swept
404
+ (`TM_MEMORY_SESSION_TTL_MIN`, default 240 min), invisible to other
405
+ sessions; ephemeral unless `TM_MEMORY_SESSION_PERSIST=1` writes them under
406
+ `memories/sessions/<sid>/`.
407
+ - **`project`** (default): `<repo>/.git/opencode-team/memories/…` — per checkout, git-adjacent. Facts about THIS repo: build commands, environment quirks, architecture decisions.
408
+ - **`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.
409
+
410
+ Actions: `add` / `search` (deterministic keyword scoring) / `list` / `forget` /
411
+ `compact`; 4000 chars per memory. `search` walks ALL tiers with **session >
412
+ project > global precedence** — project entries get a +2 near-tie weight and a
413
+ same-title higher-layer twin shadows the lower one (it never surfaces).
414
+ Near-duplicates never pile up: an `add` that hits an existing entry in the same
415
+ tier and category (dedup key, or title+keywords Jaccard ≥ 0.6) **folds into
416
+ it** — new content wins, keywords union, the old slug moves to `supersedes:`,
417
+ and the answer says 已合并 (that is normal; don't re-add under a variant title).
418
+ When a tier reaches `TM_MEMORY_MAX_ENTRIES` (200 per scope) the add fails on
419
+ purpose: run `compact` first — it dry-runs the merge plan by default, and
420
+ re-running with `apply:true` performs it after copying every original to a
421
+ timestamped `.compact-backup` tree (the rollback path). Entries older than
422
+ `TM_MEMORY_STALE_DAYS` (30) surface tagged `[stale Nd]` in search. Agents
423
+ are prompted to search before assuming conventions and to save hard-won
424
+ facts for the next conversation.
425
+
426
+ ### 🌐 Web search that actually works (in China)
427
+
428
+ `tm_search` is the open-ended-lookup front: **one call, one query, clean
429
+ results**. The engine URL is built for you, fetched through the governed
430
+ pipeline, and collapsed into a numbered title+URL hit list — the agent never
431
+ sees raw SERP chrome.
432
+
433
+ | Engine | Notes |
434
+ |---|---|
435
+ | `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` |
436
+ | `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 |
437
+ | `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 |
438
+ | `hn` | Hacker News via Algolia API (no key) → story titles + snippets with direct article URLs |
439
+ | `bilibili` | video search |
440
+ | `moegirl` | MediaWiki search API — entry titles + snippets, structured |
441
+ | `npm` | registry search → name@version + description, structured |
442
+ | `github` | repo search API → stars + description, structured; `org:` / `user:` / `stars:` / `language:` qualifiers fold into the query (e.g. `vector db stars:>500 language:rust`) |
443
+
444
+ All seven engines are reachable from mainland China **without API keys**, and
445
+ every one of them sits on the seeded domain allowlist. The previous CN HTML
446
+ SERPs (`sogou` / `so` / `baidu` / `bing-int`) were **removed** — a live
447
+ benchmark (2026-09-14) showed them serving anti-bot shells or 100% empty
448
+ results; they are not even manually selectable. On an empty result,
449
+ the error names the alternative engines instead of leaving the agent stuck.
450
+ Two more channels complete the surface:
451
+
452
+ - `tm_webfetch` — a known URL, one governed GET. Search-engine pages it
453
+ fetches are auto-extracted to hit lists too. JSON endpoints like
454
+ `registry.npmjs.org/<pkg>/latest` pass through untouched.
455
+ - `tm_browser` — JS-rendered pages: **your DEFAULT browser** (Windows
456
+ registry / Linux `xdg-settings`; Chromium-family only — Firefox falls back
457
+ to the Edge/Chrome probe order because CDP is Chromium-proprietary;
458
+ `TM_BROWSER_PATH` overrides), headful by default, isolated temp profile,
459
+ **domain allowlist enforced at the network layer** per request and per
460
+ redirect hop. The action surface is chrome-devtools-mcp aligned: **16
461
+ Playwright verbs** (`navigate_page` · `take_snapshot` · `click` · `fill` ·
462
+ `hover` · `drag` · `press_key` · `select_page` · `upload_file` · `wait_for`
463
+ · `evaluate_script` · `list_console_messages` · `list_network_requests` ·
464
+ `list_pages` · `take_screenshot` · `handle_dialog`) plus 5 legacy compat
465
+ verbs (`open` / `navigate` / `read` / `screenshot` / `close`).
466
+ Snapshot-first: `take_snapshot` returns the aria snapshot with injected
467
+ `[uid=eN]` tokens, follow-up actions address nodes by uid instead of
468
+ guessed locators; snapshots are hard-capped by
469
+ `TM_BROWSER_SNAPSHOT_MAX_TOKENS` (default 1200).
470
+ **Engine split:** the primary engine is `playwright-core` (an optional
471
+ dependency — needs **Node ≥ 20**; on older Node, or if the import fails,
472
+ the session auto-degrades per instance to the zero-dep `cdp-legacy`
473
+ engine, which keeps the core verbs only; pin either via
474
+ `TM_BROWSER_ENGINE=playwright|cdp-legacy`). No browser download is ever
475
+ involved — Playwright launches YOUR installed browser by path, so
476
+ `npx playwright install` is not part of the user flow (the dependency
477
+ itself resolves at npm install/publish time). Isolated temp profile by
478
+ default: persistent logins only if you explicitly set
479
+ `TM_BROWSER_USER_DATA_DIR`. No cookies by design — the agent opens a blank
480
+ profile, which is also why a risk-scored site (Baidu) may hand it a
481
+ verification wall your own window walks past. Want it logged in: point the
482
+ variable at a DEDICATED directory and log in once by hand. The browser's **own** site-permission bubble
483
+ ("…wants to access other apps and services on this device") is
484
+ auto-**denied** at launch: that modal is not our confirmation channel, it has
485
+ no timeout, and on a machine left running unattended it would hang the page
486
+ indefinitely. Nothing is ever auto-allowed — a page that truly needs such a
487
+ permission fails visibly at that feature instead of invisibly at a dialog. A
488
+ window an agent forgets is now **reported**, not merely reaped: `open` states
489
+ the close duty in its own reply (with the real idle seconds), the reply
490
+ contract requires the tool's close verdict in `EVIDENCE`, and `tm_join` tells
491
+ the lead `⚠ N 个浏览器还开着` when a sub-agent settled while still holding one
492
+ — so you are never the only one who notices.
493
+
494
+ When a fetch still returns **403 after the real-Chrome headers**, the error
495
+ is a DIRECTIVE: the gate is JS-challenge / TLS-fingerprint based and only a
496
+ real browser passes — the agent is told to call `tm_browser`
497
+ (`action:"open"` → `action:"read"`) for that URL. Search hit lists also
498
+ filter known noise: engine-internal wrappers (`so.com/link?`, `ai.so.com`)
499
+ and same-name-different-site domains (`maimai.cn` 脉脉 vs the maimai DX
500
+ game) never ride along — extend the hit blacklist with `TM_HIT_BLACKLIST`.
501
+
502
+ Seeded allowlist (all three web tools; 23 hosts — baidu/moegirl/bilibili are
503
+ PARENT domains, so every sibling subdomain — baike.baidu.com,
504
+ mzh.moegirl.org.cn, space.bilibili.com — is covered):
505
+ `baidu.com`, `moegirl.org.cn`, `bilibili.com`, `www.sogou.com`, `www.so.com`,
506
+ `cn.bing.com`, `www.bing.com`, `zhihu.com`, `juejin.cn`, `csdn.net`,
507
+ `cnblogs.com`, `gitee.com`, `github.com`, `api.github.com`,
508
+ `raw.githubusercontent.com`, `gist.githubusercontent.com`, `ghproxy.net`
509
+ (mainland mirror for github raw), `stackoverflow.com`, `api.stackexchange.com`
510
+ + `hn.algolia.com` (the two JSON search engines), `npmjs.org`, `pypi.org`,
511
+ `learn.microsoft.com` — extend via
512
+ `TM_WEBFETCH_ALLOWED_DOMAINS` (`"*"` opens every host; a custom list REPLACES
513
+ the seed, so keep the engine hosts or `tm_search` loses its targets). Architect /
514
+ implementer / reviewer have NO network grant — web questions come back as a
515
+ reported gap, never simulated. The tester carries `tm_browser` ONLY, for
516
+ governed UI verification of the project (local dev servers, preview routes);
517
+ open web fetching stays with the two network roles.
518
+
519
+ **Out-of-allowlist targets are a gate, not a wall.** When a fetch / search /
520
+ browser-open points at a host outside the allowlist, the tool hands the URL
521
+ to OpenCode's **official confirmation dialog** — you decide, once per
522
+ target (an unanswered dialog is auto-rejected on the usual 1-minute timer,
523
+ and the plugin still never self-allows). Every dialog also fires a
524
+ **system toast notification**, so you know something is waiting even when
525
+ you're not staring at the screen. Env-file URLs and non-http(s) schemes
526
+ remain hard-rejected with no dialog — R6 red lines are never consentable.
527
+
528
+ ### Security: the R6 + R2 approval gate
529
+
530
+ **R6 environment protection.** With TeamMode active, the model cannot read
531
+ environment variables silently. Env reads (`printenv`, `env`, `Get-ChildItem
532
+ env:`, …) and env files (`.env`, shell rc) route through OpenCode's official
533
+ confirmation dialog; unanswered prompts are **auto-rejected after
534
+ `TM_ASK_TIMEOUT_MIN` (default 1 min)**. Env reads no wildcard can express
535
+ (embedded `$VAR` / `${VAR}` / `$env:` inside another command, command
536
+ substitution) and the `tm_*` wrapper channel stay a **hard block** — no dialog
537
+ to slip through. The audit log records only tool name + pattern category +
538
+ verdict — never command text, paths, variable names or values. Verdicts now
539
+ also explain a dialog that cannot be answered anymore: a reply that hits an
540
+ **already-closed** dialog (host 404 — the timer already auto-rejected) audits
541
+ as `already-closed` without degrading the gate, a plugin-side bad reply shape
542
+ audits as `rejected-shape-bug`, and a user reply that arrives *after* the
543
+ auto-reject is recorded as `late-<verdict>` for observability only (the
544
+ rejection stands — the plugin still never self-allows). The 1-minute auto-reject
545
+ floor is tunable via `TM_ASK_TIMEOUT_FLOOR_MIN`; the short default is safe
546
+ because a reply that races the timer audits as benign `already-closed` (host
547
+ 404 on a closed dialog — no gate degrade), and the observed ~120 s is the
548
+ host-to-plugin event-bus *delivery* lag, not click-resolution latency.
549
+
550
+ **R2 dangerous operations (same dialog).** Delete, git publish, network
551
+ fetch, package install/publish, process/system, privilege changes — none are
552
+ silently allowed. The normal verification stack (`npm test`, `tsc`,
553
+ `git status`) is NOT gated, so day-to-day work runs uninterrupted.
554
+
555
+ > ⚠️ **When you approve a dialog, pick "once" — not "always".** Verified on
556
+ > the live host, "always" records a far broader rule than the command you
557
+ > saw: approving `Get-ChildItem env:PATH` with "always" stores `Get-ChildItem
558
+ > *`, so every later `Get-ChildItem` runs with no dialog at all. The web
559
+ > channel has the same trap and it is easier to fall into: one "always" on a
560
+ > host lets **every agent session in that project** open it, so the per-agent
561
+ > browser consent never gets asked again. The reply now says which path let a
562
+ > page in (static allowlist / the dialog you just answered / a saved rule that
563
+ > answered in milliseconds), because an agent that cannot tell them apart
564
+ > reports "no dialog" as "that site is allowed".
565
+
566
+ > Deferral to the dialog is per-session: env reads only route to the dialog
567
+ > in sessions running TeamMode's injected agents. In any other session the
568
+ > guard keeps hard-blocking env reads. `headless opencode run` auto-rejects
569
+ > unanswered `ask`s immediately (there is no human to prompt).
570
+
571
+ ### Repo hygiene
572
+
573
+ The offload/trajectory/memory stores live under `<repo>/.git/opencode-team/`
574
+ (outside a git repo: the OS temp dir, sharded per workspace as
575
+ `opencode-team/w-<hash>/`, so one project never reads another's payloads) —
576
+ **never your working tree**. Every
577
+ agent is instructed to delete scratch files before reporting done and to keep
578
+ throwaway work in the OS temp dir. A TTL sweeper reclaims old task dirs at
579
+ startup + hourly; the Team Lead never deletes boards itself, so you can audit
580
+ any run.
581
+
582
+ ---
583
+
584
+ ## ⚙️ Configuration
585
+
586
+ The plugin injects everything at startup — no agent files to copy.
587
+
588
+ > **Model choice matters.** Every judgment — triage, decomposition, dispatch
589
+ > briefs, synthesis, review verdicts — flows through the Team Lead. A weak
590
+ > model in that seat degrades the whole pipeline no matter how strong the
591
+ > specialists are. Pin your best reasoning model to `team`:
592
+
593
+ ```jsonc
594
+ {
595
+ "agent": {
596
+ "team": { "model": "anthropic/claude-opus-4-5" }, // Lead earns your best model
597
+ "implementer": { "model": "anthropic/claude-sonnet-4-6" } // specialists tolerate cheaper
598
+ }
599
+ }
600
+ ```
601
+
602
+ **Global install** — put the plugin entry in `~/.config/opencode/opencode.jsonc` and every project gets the team.
603
+
604
+ **Keep Team off the default slot:**
605
+
606
+ ```jsonc
607
+ {
608
+ "plugin": [
609
+ ["@te-river/opencode-team-mode@latest", { "defaultAgent": false }]
610
+ ]
611
+ }
612
+ ```
613
+
614
+ Your own agents named `team` / `architect` / … always take precedence; the
615
+ plugin never clobbers user definitions. See [Customization](#-customization)
616
+ for overrides, extra agents and disabling roles.
617
+
618
+ ### Environment variables
619
+
620
+ | Env var | Default | Purpose |
621
+ |---|---|---|
622
+ | `TM_ENV_PROTECT` | `strict` | R6 mode: `strict` / `standard` / `off` (off also disarms the approval timer) |
623
+ | `TM_R6_FINE_ASK` | classifier (v2 only) | On **v2** 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. v1 is unaffected: it classifies in the tool-call hook either way |
624
+ | `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 |
625
+ | `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 |
626
+ | `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 |
627
+ | `TM_PRIVATE_SPACE` | `ask` on v1 · `deny` on **v2** | What private space (loopback, RFC1918, ULA, CGNAT, `.localhost`) does through our tools: `ask` routes it to the host dialog (v1 can raise one), `allow` opens the class, `deny` refuses it. v2 defaults to `deny` because a plugin there 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 |
628
+ | `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 |
629
+ | `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 |
630
+ | `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 |
631
+ | `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 |
632
+ | `TM_ASK_TIMEOUT_FLOOR_MIN` | `1` | minimum enforced for the ask timeout above |
633
+ | `TM_ENV_PROTECT_EXTRA_DENY` | — | extra block patterns (regex; always hard block, never dialog-governed) |
634
+ | `TM_OFFLOAD_THRESHOLD` | `2000` | global offload fallback (tokens, CJK-aware estimate) — used when the content class is unknown |
635
+ | `TM_OFFLOAD_THRESHOLD_TEXT` | `4000` | offload boundary for prose (text / log / markdown) |
636
+ | `TM_OFFLOAD_THRESHOLD_DATA` | `2000` | offload boundary for structured payloads (json / csv / code / binary) |
637
+ | `TM_PREVIEW_MAX_TOKENS` | `80` | preview hard cap |
638
+ | `TM_FETCH_MAX_LINES` | `2000` | tm_fetch page cap |
639
+ | `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) |
640
+ | `TM_BLACKBOARD_TTL` | `7` | store retention (days) |
641
+ | `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 |
642
+ | `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 |
643
+ | `TM_BASH_READONLY_ALLOWED` | built-in table | tm_bash allowlist |
644
+ | `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) |
645
+ | `TM_WEBFETCH_ALLOWED_DOMAINS` | the 24 seeded hosts on v1; `"*"` on **v2** | tm_webfetch / tm_search / tm_browser allowlist (`"*"` opens all; empty = deny all; a custom list REPLACES the seed — keep the engine hosts). A site's own asset CDN has to be seeded or `tm_browser` renders it blank — `bdimg.com` is there for exactly that reason; per-session gaps go through `tm_browser { action:"allow_host", host }` instead of an env edit. `"*"` 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 |
646
+ | `TM_BROWSER_PATH` | auto-detect | tm_browser executable override (default: your DEFAULT browser when Chromium-family, else Edge/Chrome probes) |
647
+ | `TM_BROWSER_HEADLESS` | `auto` | `1` headless (CI) / `0` headful / `auto` (headless only on display-less Linux) |
648
+ | `TM_BROWSER_ENGINE` | `playwright` | `playwright` (needs Node ≥ 20; any import failure auto-degrades) / `cdp-legacy` (zero-dep CDP pipe, core verbs only) |
649
+ | `TM_BROWSER_SNAPSHOT_MAX_TOKENS` | `1200` | hard cap on `take_snapshot` payloads |
650
+ | `TM_BROWSER_SUBRESOURCE` | `same-site` | what a page may load after its navigation was allowed: `same-site` = images/media/fonts/stylesheets always, scripts/XHR only for a site this session actually opened; `passive` = only the passive types; `off` = the legacy every-request gate. Blocked requests surface as a "N 个子资源请求被拦截" note on the next snapshot, and when the page has nothing addressable left the note says the blankness is ours and points at `allow_host` / this env var |
651
+ | `TM_BROWSER_IDLE_MS` | `180000` | an untouched browser session closes itself after this many ms (0 disables) and tells the user — a window nobody owns is a user-facing bug |
652
+ | `TM_BROWSER_ASK_EVAL` | `on` | `evaluate_script` runs arbitrary JS in YOUR browser — the one verb the domain allowlist can't cover (it limits where we navigate, not what a loaded page hands back). One official-dialog consent per browser session; no ask bridge ⇒ refused. `off` restores the old behaviour; the result redaction (JWT/bearer/cookie/api-key shapes) is NOT switchable |
653
+ | `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 / the PTC bridge; 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 缓存命中 |
654
+ | `TM_BROWSER_IMAGE_MAX_BYTES` | `400000` | ceiling on the JPEG `take_screenshot { image:true }` inlines into the model's context (above it the reply stays path-only and says why) |
655
+ | `TM_SEARCH_WEIGHTS` | unset | per-engine fusion weight overrides, e.g. `bing=0.3,hn=0.25`; anything unset keeps the built-in table |
656
+ | `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) |
657
+ | `TM_SEARCH_MAX_HITS` | `10` | hits kept per engine leg and in the fused list |
658
+ | `TM_SEARCH_DISABLED_ENGINES` | unset | engines removed from the roster AND from every `auto` route (`sogou,baidu` style) |
659
+ | `TM_BASH_TIMEOUT_PROBE_MS` | `60000` | ceiling forced onto a `timeout` the model set for a read-only probe command (0 disables) |
660
+ | `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 |
661
+ | `TM_PTY_MAX` | `4` | concurrent `tm_pty` terminal sessions this plugin may keep running at once |
662
+ | `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 |
663
+ | `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) |
664
+ | `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` |
665
+ | `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) |
666
+ | `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 |
667
+ | `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 |
668
+ | `TM_COMPACTION_AUTOCONTINUE` | `on` | `off` stops the host from silently resuming the turn after a compaction, so a human re-reads state first |
669
+ | `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 |
670
+ | `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 |
671
+ | `TM_BROWSER_USER_DATA_DIR` | — (isolated temp profile) | explicit persistent profile dir — the ONLY way logins survive between sessions, and honored by BOTH engines (playwright and cdp-legacy). Use a DEDICATED directory (e.g. `D:\tm-browser-profile`) and log in by hand the first time; pointing it at your browser's own data dir (`…\Microsoft\Edge\User Data`, `google-chrome`, Firefox `Profiles`) is refused before anything spawns, because that would have the agent browse as you while a force-kill reaper owns the process |
672
+ | `TM_MEMORY_GLOBAL_DIR` | `~/.opencode-team/memories/global/` | tm_memory GLOBAL tier store |
673
+ | `TM_MEMORY_SESSION_TTL_MIN` | `240` | session-tier entry TTL (lazy + boot sweep) |
674
+ | `TM_MEMORY_MAX_ENTRIES` | `200` | per-scope entry cap; over it `add` fails on purpose — run `compact` |
675
+ | `TM_MEMORY_STALE_DAYS` | `30` | age after which search hits are tagged `[stale Nd]` (`0` disables) |
676
+ | `TM_MEMORY_SESSION_PERSIST` | — (ephemeral) | `1` also writes session entries under `memories/sessions/<sid>/` |
677
+ | `TM_HIT_BLACKLIST` | `maimai.cn` | extra domains never listed as search hits (comma/semicolon separated; same-name-different-site noise like 脉脉) |
678
+ | `TM_PTC_MAX_PROGRAM_CHARS` | `4000` | PTC program source cap |
679
+ | `TM_PTC_MAX_CALLS` | `20` | PTC per-run bridge-call budget (1–200) |
680
+ | `TM_PTC_MAX_ERRORS` | `3` | PTC per-run error budget (1–50) |
681
+ | `TM_PTC_TIMEOUT_MS` | `60000` | PTC per-run wall-clock timeout (5s–10min) |
682
+ | `TM_PTC_ENGINE` | `auto` | `auto` (worker→inline degrade) / `worker` / `inline` |
683
+ | `TM_PTC_WEB_BRIDGE` | `on` | expose `tm.search` / `tm.webfetch` to PTC programs (`off` removes them from the bridge set) |
684
+
685
+ ---
686
+
687
+ ## 🏗️ How the team works
688
+
689
+ Sub-agents can't message each other live (platform limitation), so TeamMode
690
+ coordinates them through a **structured reply skeleton** — every specialist
691
+ reply is `STATUS: / CHANGES: / FINDINGS: / EVIDENCE: / HANDOFF:`, ≤50 lines,
692
+ relayed verbatim by the Lead into the next dispatch. Files are the exception,
693
+ not the rule: a deliverable over ~50 lines goes to ONE named board file under
694
+ `<repo>/.git/opencode-team/` (round-suffixed, working tree untouched).
695
+
696
+ - **Routing table:** question → direct answer; docs-only → implementer;
697
+ product change → implementer → tester → reviewer; multi-module →
698
+ architect → implementer → tester → reviewer(s); unknown tech → researcher
699
+ first. Fixed minimums — a product change routed below 3 dispatches is a
700
+ routing bug.
701
+ - **Approval gate (count-based):** ≥2 dispatches → plan (≤30 lines) → **your
702
+ approval** → execute. Blocking questions are batched and asked immediately.
703
+ - **Concurrent dispatch:** independent dispatches batch into the SAME round
704
+ (parallel implementers with per-file ownership + verbatim contracts,
705
+ 3-dimension reviews, split test suites); only the Team Lead dispatches —
706
+ specialists no longer hold `task`, so no sub-agent spawns a sub-agent.
707
+ - **Adaptive review:** one reviewer by default; three parallel dimensions
708
+ only for high-risk profiles (auth/security, cross-module contracts, public APIs).
709
+ - **Static verification:** build / typecheck / lint / tests. Improvised
710
+ browser automation is banned; unverified UI work ends with
711
+ `UI NOT VERIFIED: <what to check>`.
712
+ - **Evidence standard:** "done / fixed / passed" claims need verifiable
713
+ evidence — output, logs, diffs.
714
+
715
+ ---
716
+
717
+ ## 🔧 Customization
718
+
719
+ **Override an agent** — same name in your config wins:
720
+
721
+ ```jsonc
722
+ {
723
+ "agent": {
724
+ "reviewer": {
725
+ "model": "anthropic/claude-sonnet-4-6",
726
+ "prompt": "You are an extremely strict reviewer. Reject anything with a lint warning."
727
+ }
728
+ }
729
+ }
730
+ ```
731
+
732
+ **Add your own agents** alongside the team:
733
+
734
+ ```jsonc
735
+ {
736
+ "agent": {
737
+ "devops": {
738
+ "mode": "subagent",
739
+ "description": "Handles CI/CD, Docker, and deployment tasks.",
740
+ "prompt": "You are the DevOps engineer..."
741
+ }
742
+ }
743
+ }
744
+ ```
745
+
746
+ **Disable one:** `"researcher": { "disable": true }`.
747
+
748
+ **Board retention** via the tuple form: `["@te-river/opencode-team-mode@latest", { "ttlDays": 7 }]` (valid range (0, 365], invalid values fall back to 5).
749
+
750
+ ---
751
+
752
+ ## ❓ FAQ
753
+
754
+ **Will this eat my tokens?**
755
+ The opposite is the point. Offload + 80-token previews + PTC batch programs
756
+ exist because a five-agent pipeline naively bolted onto one context window
757
+ *would* eat your tokens. The governance is the token-saver.
758
+
759
+ **Is the web access safe?**
760
+ It's the most guarded surface in the plugin: two full web roles plus a
761
+ browser-only tester grant, a domain
762
+ allowlist with dialog-gated escapes (you approve any out-of-allowlist
763
+ target in OpenCode's official dialog, with a toast notification), redirects
764
+ re-checked per hop, network-layer enforcement in the browser, env-file URL
765
+ refusal, and every payload rides the same offload governance. No allowlisted
766
+ page can bounce the fetch off-site.
767
+
768
+ **Why doesn't the plugin auto-update?**
769
+ OpenCode caches plugins by spec string and never re-resolves `@latest`
770
+ (upstream limitation, not ours). **Re-run the installer — that IS the
771
+ update** (it purges the cache and re-resolves npm copies); or delete the
772
+ cache dir by hand. Recipe above and in the
773
+ [installation guide](./docs/installation.md).
774
+
775
+ **Can agents run tools in parallel?**
776
+ Yes — and they're *engineered* for it: parallel `tm_search` / `tm_webfetch` /
777
+ `tm_fetch` calls get distinct step ids and isolated payloads. A regression
778
+ here fails the test suite before it ever reaches you.
779
+
780
+ **Does it work in the CLI (TUI), or only Desktop?**
781
+ Both. Desktop adds the color-coded picker and panels; the governed tools and
782
+ the whole workflow are host-agnostic. On display-less Linux, `tm_browser`
783
+ runs headless automatically.
784
+
785
+ **What happens if I don't answer a confirmation dialog?**
786
+ It auto-rejects after `TM_ASK_TIMEOUT_MIN` (default 1). The plugin never
787
+ self-approves — the only side it can take is yours or nobody's.
788
+
789
+ ---
790
+
791
+ ## 🗑️ Uninstall
792
+
793
+ 1. Remove the entry from the `"plugin"` array in your config file — **`"plugins"` (plural) on OpenCode 2.x**, which is also where the 2.x installers write; 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.
794
+ 2. Delete the cache dir (table in [Install](#-read-this-once-save-yourself-an-hour-later)) if you want the disk space back.
795
+ 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.
796
+
797
+ No DLLs were harmed. Nothing was written to your working tree.
798
+
799
+ ---
800
+
801
+ ## 🏛️ Architecture (for the curious)
802
+
803
+ ```
804
+ opencode-team-mode/
805
+ ├── src/
806
+ │ ├── index.ts ← Plugin entry (config + R6 guard + approval gate + tool segment)
807
+ │ ├── agents.ts ← Agent structure (modes, colors, temperatures, whitelist matrix)
808
+ │ ├── prompts/ ← Agent prompts (lead / specialists / shared) — pinned by tests
809
+ │ ├── commands.ts ← Slash command definitions
810
+ │ ├── blackboard.ts ← Shared blackboard + TTL sweeper
811
+ │ ├── envprotect.ts ← R6 facade (patterns / classifiers / gate predicates / hook)
812
+ │ ├── approval-gate.ts ← Unified approval gate (dialog timeout auto-reject)
813
+ │ ├── tm/ ← Governed tools: pipelines / store / preview / guard / refs /
814
+ │ │ webfetch / search / memory / browser / shell-bridge / ptc/ (9 modules)
815
+ │ └── types.ts ← Loader-contract types (1.18.x)
816
+ ├── docs/installation.md ← The agent-consumable install guide (1.18.x)
817
+ ├── docs/installation-v2.md ← The 2.x install guide (roles arrive as generated config files)
818
+ ├── scripts/ ← One-line installers (bash / PowerShell)
819
+ ├── pt07/ ← PT-07 baseline suite (seeded A/B token measurement)
820
+ └── README.*.md ← You are here (twice)
821
+ ```
822
+
823
+ The loader calls `server(input, options)` once: the `config` hook injects the
824
+ six agents and six commands, the same call installs the R6
825
+ `tool.execute.before` guard, arms the approval gate through an `event` hook,
826
+ and registers the `tm_*` tools. User-defined agents with the same name always
827
+ win — the plugin never clobbers.
828
+
829
+ ---
830
+
831
+ ## 🤝 Contributing
832
+
833
+ Issues and PRs welcome. Especially wanted: localization of agent prompts,
834
+ more agent roles, more command templates, and real-world reports of the
835
+ search engines' behavior (they rearrange their markup; the extractor filters
836
+ are intentionally loose but not psychic).
837
+
838
+ ---
839
+
840
+ ## 📄 License
841
+
842
+ [Apache License 2.0](./LICENSE)
843
+
844
+ ---
845
+
846
+ ## 🔗 Links
847
+
848
+ - [npm Package](https://www.npmjs.com/package/@te-river/opencode-team-mode) — `@te-river/opencode-team-mode`
849
+ - [Installation guide, 1.18.x](./docs/installation.md) — the complete manual / agent-consumable procedure
850
+ - [Installation guide, **OpenCode 2.x**](./docs/installation-v2.md) — a different install: a 2.x plugin cannot create an agent, so the six roles and six commands are generated into the config directory
851
+ - [OpenCode Desktop](https://opencode.ai) — Official website & download
852
+ - [OpenCode Docs](https://opencode.ai/docs) — Configuration & plugin documentation
853
+ - [OpenCode Plugin API](https://opencode.ai/docs/plugins) — Build your own plugins