@te-river/opencode-team-mode 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/README.md +117 -118
  2. package/README.zh-CN.md +98 -100
  3. package/dist/agents.d.ts.map +1 -1
  4. package/dist/agents.js +89 -79
  5. package/dist/agents.js.map +1 -1
  6. package/dist/approval-gate.d.ts +8 -9
  7. package/dist/approval-gate.d.ts.map +1 -1
  8. package/dist/approval-gate.js +13 -14
  9. package/dist/approval-gate.js.map +1 -1
  10. package/dist/capabilities.d.ts +11 -37
  11. package/dist/capabilities.d.ts.map +1 -1
  12. package/dist/capabilities.js +11 -141
  13. package/dist/capabilities.js.map +1 -1
  14. package/dist/commands.js +1 -1
  15. package/dist/envprotect/hook.d.ts +12 -31
  16. package/dist/envprotect/hook.d.ts.map +1 -1
  17. package/dist/envprotect/hook.js +13 -91
  18. package/dist/envprotect/hook.js.map +1 -1
  19. package/dist/envprotect/patterns.d.ts +6 -4
  20. package/dist/envprotect/patterns.d.ts.map +1 -1
  21. package/dist/envprotect/patterns.js +6 -4
  22. package/dist/envprotect/patterns.js.map +1 -1
  23. package/dist/envprotect.d.ts +1 -1
  24. package/dist/envprotect.d.ts.map +1 -1
  25. package/dist/envprotect.js +1 -1
  26. package/dist/envprotect.js.map +1 -1
  27. package/dist/host/v2-browser-gate.d.ts +19 -1
  28. package/dist/host/v2-browser-gate.d.ts.map +1 -1
  29. package/dist/host/v2-browser-gate.js +47 -6
  30. package/dist/host/v2-browser-gate.js.map +1 -1
  31. package/dist/host/v2-capabilities.d.ts +33 -0
  32. package/dist/host/v2-capabilities.d.ts.map +1 -1
  33. package/dist/host/v2-capabilities.js +50 -1
  34. package/dist/host/v2-capabilities.js.map +1 -1
  35. package/dist/host/v2-compaction.d.ts +129 -0
  36. package/dist/host/v2-compaction.d.ts.map +1 -0
  37. package/dist/host/v2-compaction.js +346 -0
  38. package/dist/host/v2-compaction.js.map +1 -0
  39. package/dist/host/v2-default-agent.d.ts +37 -0
  40. package/dist/host/v2-default-agent.d.ts.map +1 -0
  41. package/dist/host/v2-default-agent.js +128 -0
  42. package/dist/host/v2-default-agent.js.map +1 -0
  43. package/dist/host/v2-events.d.ts +2 -0
  44. package/dist/host/v2-events.d.ts.map +1 -1
  45. package/dist/host/v2-events.js +27 -0
  46. package/dist/host/v2-events.js.map +1 -1
  47. package/dist/host/v2-guard.d.ts +178 -0
  48. package/dist/host/v2-guard.d.ts.map +1 -1
  49. package/dist/host/v2-guard.js +324 -8
  50. package/dist/host/v2-guard.js.map +1 -1
  51. package/dist/host/v2-offload.d.ts +1 -1
  52. package/dist/host/v2-offload.js +2 -2
  53. package/dist/host/v2-permissions.d.ts +45 -44
  54. package/dist/host/v2-permissions.d.ts.map +1 -1
  55. package/dist/host/v2-permissions.js +48 -49
  56. package/dist/host/v2-permissions.js.map +1 -1
  57. package/dist/host/v2-probe-chain.d.ts +93 -0
  58. package/dist/host/v2-probe-chain.d.ts.map +1 -0
  59. package/dist/host/v2-probe-chain.js +146 -0
  60. package/dist/host/v2-probe-chain.js.map +1 -0
  61. package/dist/host/v2-prune.d.ts +157 -0
  62. package/dist/host/v2-prune.d.ts.map +1 -0
  63. package/dist/host/v2-prune.js +507 -0
  64. package/dist/host/v2-prune.js.map +1 -0
  65. package/dist/host/v2-retry.d.ts +143 -0
  66. package/dist/host/v2-retry.d.ts.map +1 -0
  67. package/dist/host/v2-retry.js +315 -0
  68. package/dist/host/v2-retry.js.map +1 -0
  69. package/dist/host/v2-scope.d.ts.map +1 -1
  70. package/dist/host/v2-scope.js +14 -7
  71. package/dist/host/v2-scope.js.map +1 -1
  72. package/dist/host/v2-session-client.d.ts +24 -0
  73. package/dist/host/v2-session-client.d.ts.map +1 -1
  74. package/dist/host/v2-session-client.js +243 -3
  75. package/dist/host/v2-session-client.js.map +1 -1
  76. package/dist/host/v2-session.d.ts +72 -1
  77. package/dist/host/v2-session.d.ts.map +1 -1
  78. package/dist/host/v2-session.js +154 -10
  79. package/dist/host/v2-session.js.map +1 -1
  80. package/dist/host/v2-tool.d.ts +2 -3
  81. package/dist/host/v2-tool.d.ts.map +1 -1
  82. package/dist/host/v2-tool.js +2 -3
  83. package/dist/host/v2-tool.js.map +1 -1
  84. package/dist/host/v2-types.d.ts +25 -0
  85. package/dist/host/v2-types.d.ts.map +1 -1
  86. package/dist/host/v2.d.ts +14 -12
  87. package/dist/host/v2.d.ts.map +1 -1
  88. package/dist/host/v2.js +500 -73
  89. package/dist/host/v2.js.map +1 -1
  90. package/dist/identity.d.ts +27 -0
  91. package/dist/identity.d.ts.map +1 -0
  92. package/dist/identity.js +34 -0
  93. package/dist/identity.js.map +1 -0
  94. package/dist/index.d.ts +9 -24
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +17 -27
  97. package/dist/index.js.map +1 -1
  98. package/dist/prompts/lead.d.ts +1 -1
  99. package/dist/prompts/lead.d.ts.map +1 -1
  100. package/dist/prompts/lead.js +38 -25
  101. package/dist/prompts/lead.js.map +1 -1
  102. package/dist/prompts/shared.d.ts +2 -2
  103. package/dist/prompts/shared.d.ts.map +1 -1
  104. package/dist/prompts/shared.js +66 -56
  105. package/dist/prompts/shared.js.map +1 -1
  106. package/dist/prompts/specialists.d.ts +2 -2
  107. package/dist/prompts/specialists.d.ts.map +1 -1
  108. package/dist/prompts/specialists.js +53 -83
  109. package/dist/prompts/specialists.js.map +1 -1
  110. package/dist/tm/args-schema.d.ts +7 -32
  111. package/dist/tm/args-schema.d.ts.map +1 -1
  112. package/dist/tm/args-schema.js +12 -153
  113. package/dist/tm/args-schema.js.map +1 -1
  114. package/dist/tm/board.d.ts +10 -9
  115. package/dist/tm/board.d.ts.map +1 -1
  116. package/dist/tm/board.js +11 -10
  117. package/dist/tm/board.js.map +1 -1
  118. package/dist/tm/config-files.d.ts +81 -0
  119. package/dist/tm/config-files.d.ts.map +1 -0
  120. package/dist/tm/config-files.js +209 -0
  121. package/dist/tm/config-files.js.map +1 -0
  122. package/dist/tm/config-layers.d.ts +117 -0
  123. package/dist/tm/config-layers.d.ts.map +1 -0
  124. package/dist/tm/config-layers.js +320 -0
  125. package/dist/tm/config-layers.js.map +1 -0
  126. package/dist/tm/config.d.ts +10 -101
  127. package/dist/tm/config.d.ts.map +1 -1
  128. package/dist/tm/config.js +17 -71
  129. package/dist/tm/config.js.map +1 -1
  130. package/dist/tm/dispatch.d.ts +115 -21
  131. package/dist/tm/dispatch.d.ts.map +1 -1
  132. package/dist/tm/dispatch.js +424 -67
  133. package/dist/tm/dispatch.js.map +1 -1
  134. package/dist/tm/egress.d.ts +19 -7
  135. package/dist/tm/egress.d.ts.map +1 -1
  136. package/dist/tm/egress.js +21 -9
  137. package/dist/tm/egress.js.map +1 -1
  138. package/dist/tm/index.d.ts +23 -20
  139. package/dist/tm/index.d.ts.map +1 -1
  140. package/dist/tm/index.js +117 -116
  141. package/dist/tm/index.js.map +1 -1
  142. package/dist/tm/ledger.d.ts +9 -2
  143. package/dist/tm/ledger.d.ts.map +1 -1
  144. package/dist/tm/ledger.js +4 -1
  145. package/dist/tm/ledger.js.map +1 -1
  146. package/dist/tm/perm-ask.d.ts +3 -3
  147. package/dist/tm/perm-ask.d.ts.map +1 -1
  148. package/dist/tm/perm-ask.js +3 -3
  149. package/dist/tm/perm-ask.js.map +1 -1
  150. package/dist/tm/pipelines.d.ts +12 -11
  151. package/dist/tm/pipelines.d.ts.map +1 -1
  152. package/dist/tm/pipelines.js +40 -207
  153. package/dist/tm/pipelines.js.map +1 -1
  154. package/dist/tm/preview.d.ts +1 -1
  155. package/dist/tm/preview.js +3 -3
  156. package/dist/tm/preview.js.map +1 -1
  157. package/dist/tm/refs.d.ts +31 -4
  158. package/dist/tm/refs.d.ts.map +1 -1
  159. package/dist/tm/refs.js +48 -4
  160. package/dist/tm/refs.js.map +1 -1
  161. package/dist/tm/result.d.ts +1 -1
  162. package/dist/tm/result.d.ts.map +1 -1
  163. package/dist/tm/result.js +4 -3
  164. package/dist/tm/result.js.map +1 -1
  165. package/dist/tm/search.d.ts +2 -2
  166. package/dist/tm/search.js +7 -7
  167. package/dist/tm/search.js.map +1 -1
  168. package/dist/tm/serp-loop.d.ts +19 -7
  169. package/dist/tm/serp-loop.d.ts.map +1 -1
  170. package/dist/tm/serp-loop.js +24 -9
  171. package/dist/tm/serp-loop.js.map +1 -1
  172. package/dist/tm/stats.d.ts +12 -27
  173. package/dist/tm/stats.d.ts.map +1 -1
  174. package/dist/tm/stats.js +43 -45
  175. package/dist/tm/stats.js.map +1 -1
  176. package/dist/tm/store.d.ts +18 -5
  177. package/dist/tm/store.d.ts.map +1 -1
  178. package/dist/tm/store.js +39 -9
  179. package/dist/tm/store.js.map +1 -1
  180. package/dist/tm/tools.d.ts +7 -5
  181. package/dist/tm/tools.d.ts.map +1 -1
  182. package/dist/tm/tools.js +10 -41
  183. package/dist/tm/tools.js.map +1 -1
  184. package/dist/tm/webfetch.d.ts +1 -1
  185. package/dist/tm/webfetch.d.ts.map +1 -1
  186. package/dist/tm/webfetch.js +38 -23
  187. package/dist/tm/webfetch.js.map +1 -1
  188. package/dist/types.d.ts +40 -123
  189. package/dist/types.d.ts.map +1 -1
  190. package/dist/types.js +7 -21
  191. package/dist/types.js.map +1 -1
  192. package/docs/installation-v2.md +109 -33
  193. package/docs/installation.md +9 -0
  194. package/docs/research/browser-pane.md +5 -0
  195. package/docs/v2-port-notes.md +87 -0
  196. package/package.json +1 -1
  197. package/scripts/gen-v2-config.mjs +246 -94
  198. package/scripts/lib/fake-ctx.mjs +75 -0
package/README.md CHANGED
@@ -19,10 +19,10 @@
19
19
  > **The lazy path:** paste this to any coding agent and let it do the work:
20
20
  >
21
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
22
+ > Install the OpenCode plugin @te-river/opencode-team-mode. This package supports **OpenCode 2.x
23
+ > only** — the 1.18.x personality was removed in the 1.7.0 line:
24
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
25
+ > (a 2.x plugin cannot create an agent, so the six roles and six `/team-*` commands come
26
26
  > from a generator step in that guide — that step is not optional.) Then verify the install
27
27
  > using the checks in that guide.
28
28
  > ```
@@ -48,14 +48,14 @@ TeamMode's answer to each:
48
48
  | Pain | TeamMode's answer |
49
49
  |---|---|
50
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. |
51
+ | 🐌 **Round-trip overhead** | The host's own `execute` (Code Mode): the agent writes ONE program that makes N governed calls in a single turn. Zero LLM round-trips during the run. |
52
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
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
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
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
56
 
57
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. |
58
+ | ⏱️ **Rounds spent for the sake of looking careful** | 效率至上 is written into the lead and all five specialists: one wide call instead of three narrow ones, independent calls in the same round, ≥3 probes collapsed into one `execute` (Code Mode) program, no re-running a check to watch it pass again — with the boundary stated too: efficiency never buys its way out of the evidence rule, because an unverified "done" costs you the round *and* the bug. |
59
59
 
60
60
  And the workflow discipline underneath: deterministic routing, a ≤30-line plan
61
61
  you approve before ≥2 dispatches execute, structured `STATUS/CHANGES/FINDINGS/
@@ -75,7 +75,7 @@ projects**: on a two-file script the team simply has less to govern.
75
75
  | 🏗️ **Architect** | System designer | Design docs, module structure, API contracts |
76
76
  | 💻 **Implementer** | Code writer | Building features, writing production code |
77
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` |
78
+ | 🧪 **Tester** | Test engineer | Tests with real edge cases; static verification (build / typecheck / lint); governed UI verification via the host's native `browser_*` tools |
79
79
  | 🔎 **Researcher** | Knowledge finder | Local repo first, then the web — one of the two network roles (with the Lead) |
80
80
 
81
81
  Out of the box, **Team is your default agent** — new chats open straight into the
@@ -90,10 +90,10 @@ orchestrator (opt-out in [Configuration](#-configuration)).
90
90
  Copy this into any coding agent — it will edit your config, restart-remind you, and verify:
91
91
 
92
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
93
+ Install the OpenCode plugin @te-river/opencode-team-mode. This package supports **OpenCode 2.x
94
+ only** — the 1.18.x personality was removed in the 1.7.0 line:
95
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
96
+ (a 2.x plugin cannot create an agent, so the six roles and six `/team-*` commands come from
97
97
  a generator step in that guide — that step is not optional.)
98
98
  (If a URL is unreachable — common on mainland-China networks — retry with
99
99
  the mirror prefix: https://ghproxy.net/ + the same path.)
@@ -273,45 +273,42 @@ through with `tm_fetch` when it genuinely needs the payload.
273
273
 
274
274
  | Tool | What it does | Roles |
275
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** |
276
+ | `tm_fetch` | Paged handle retrieval for offloaded results (JSON handles take a `fields` dot-path projection — a deliberately small jq subset like `items[].name`) | all six agents |
277
277
  | `tm_memory` | Session + project + global memory store (Markdown + frontmatter): add / search / list / forget / compact | all six agents |
278
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
279
  | `tm_search` | Multi-engine web search with extracted, deduplicated, RRF-fused hit lists | Lead + Researcher |
281
280
  | `tm_webfetch` | Single governed GET of an allowlisted page (search pages auto-extracted). Manual redirects, re-checked per hop, and a refusal names the WHOLE chain (`跳转链: a → b(停在第 2 跳)`) — an allowlisted shortener that bounces off-site used to report only the off-site host, which read as "that site will not fetch" and sent the agent back to retry the entry URL it had just watched fail. A 429/503 carries its own `Retry-After` when the server sends delta-seconds (an HTTP-date is deliberately not laundered into a countdown), so "come back later" never looks like "no content here". The page GET also asks for Markdown first (`Accept: text/markdown,…`) — measured on `learn.microsoft.com`: 60,778 B of HTML becomes 11,449 B of Markdown, and every other host tried returns the same document either way, so the preference is free where it is ignored | Lead + Researcher |
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 stop a runaway, and `claimNamedChild` so `tm_join { ids: ["ses_…"] }` pulls one child's WHOLE reply back through the offload pipeline (handle + ≤80-token preview) instead of kilotokens inline. It also rebuilds its registry from the host session tree after a restart, so leftover children are 接管 rather than lost. **On 2.x it also registers the children the host's own `subagent` tool created** (the ack's `metadata.sessionID` is the seam), so a background dispatch is never reported as "nothing to collect" while the user watches it run — and such a row says where its reply actually arrives (the host's injected message) and whether it settled by event or by inference. **Stopping one is a real call on both hosts:** `cancel: true` goes to `client.session.abort` on 1.18.x and to the host's `POST /api/session/{id}/interrupt` on 2.x (`tm_join { ids: ["ses_…"], cancel: true }` stops ONE named child; without `ids` it stops every still-running one). The host's own contract is `interrupted=true` for an active execution and `false` for the idle no-op, so the answer comes back as **five distinct verdicts** — 已由宿主中断 / 空闲未中断(it wasn't running: our row was stale, not failed)/ 未确认(the call worked, no boolean came back)/ 无中断缝 / 被宿主拒绝(with the host's reason) — counted in the reply and written to the trajectory (`stop_tried` / `stop_confirmed` / `stop_refused` / `stop_unknown`, readable via `tm_stats`). It will not summarise five different facts as one "已取消", and `resume` is never sent, because resuming pending steering is the opposite of cancelling. *(On `main` only — the published 1.6.1 still answers 宿主无 abort 接口 on 2.x.)* | Lead only |
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
281
+ | `tm_ledger` | **The lead's task ledger** (`add` / `doing` / `done` / `blocked` / `list`), stored in the host's own `ctx.storage` per session — the landing spot for the LEDGER rule, since OpenCode 2.x gives a plugin no `todowrite`. A repeated ask is ONE item, an id that matches two is refused with both printed, `blocked` carries the reason, and a write that did not reach storage is reported as a failure rather than as recorded | Lead only |
282
+ | `tm_join` | **Sub-agent collection** — there is no plugin-side dispatcher any more (`tm_dispatch` is removed: a child a plugin creates is a session the user can neither open nor stop from the interface). Delegation goes through the host's own `task` / `task { background: true }`, and `tm_join` is the read side: a status snapshot, a bounded `waitMs`, `cancel:true` to stop a runaway, and `claimNamedChild` so `tm_join { ids: ["ses_…"] }` pulls one child's WHOLE reply back through the offload pipeline (handle + ≤80-token preview) instead of kilotokens inline. It also rebuilds its registry from the host session tree after a restart, so leftover children are 接管 rather than lost. **On 2.x it also registers the children the host's own `subagent` tool created** (the ack's `metadata.sessionID` is the seam), so a background dispatch is never reported as "nothing to collect" while the user watches it run — and such a row says where its reply actually arrives (the host's injected message) and whether it settled by event or by inference. **Stopping one is a real call:** `cancel: true` goes to the host's `POST /api/session/{id}/interrupt` on 2.x (`tm_join { ids: ["ses_…"], cancel: true }` stops ONE named child; without `ids` it stops every still-running one). The host's own contract is `interrupted=true` for an active execution and `false` for the idle no-op, so the answer comes back as **five distinct verdicts** — 已由宿主中断 / 空闲未中断(it wasn't running: our row was stale, not failed)/ 未确认(the call worked, no boolean came back)/ 无中断缝 / 被宿主拒绝(with the host's reason) — counted in the reply and written to the trajectory (`stop_tried` / `stop_confirmed` / `stop_refused` / `stop_unknown`, readable via `tm_stats`). It will not summarise five different facts as one "已取消", and `resume` is never sent, because resuming pending steering is the opposite of cancelling. *(On `main` only — the published 1.6.1 still answers 宿主无 abort 接口 on 2.x.)* | Lead only |
283
+ | `tm_stats` | **The plugin reads its own trajectory back**: tokens kept out of the context window by offloading (net of the preview that arrived), seconds saved by dispatch overlap (serial cost minus the wall window the children actually used), governance counts (blocked subresources, clamped shell timeouts, cache hits, redactions) — plus the **host capability matrix** (`已验证/存在未用/待观察/缺失/需人眼` per host surface). Read-only over files this plugin wrote; run it first after an OpenCode upgrade. `{ recent: 20 }` appends a call-by-call recap — handle + payload path for every offloaded result, which is how you see what a governed tool actually returned (the host gives plugin tools no expandable card) | All agents |
284
+
285
+ > **The v1 (1.18.x) personality was removed in full for the 1.7.0 line.** The package
286
+ > exports `{id, setup}` and nothing else, so `tm_read` / `tm_grep` / `tm_bash` /
287
+ > `tm_ptc_run` / `tm_pty` are gone with it — the host's own `read` / `grep` / `glob` /
288
+ > `shell` and its `execute` (Code Mode) do that work now. The governance did NOT
292
289
  > disappear with them: an oversized native result is offloaded through
293
290
  > `tool.execute.after` (measured: `shell` 12,902 tokens arriving as a 78-token
294
291
  > 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).
292
+ > host's `permission.evaluate`. `tm_ledger` is the lead's list where the host gives a
293
+ > plugin no `todowrite`.
297
294
  > **Nothing we change reaches outside Team.** Every v2 hook fires for every session on
298
295
  > the host, so the tool-surface trim, the 0.2 temperature, the blackboard note, the offload
299
296
  > of native results, the R6/address strictening and the forced-background dispatch all ask
300
297
  > "is this one of our six roles?" first — `build`, `plan` and your own agents stay as a
301
298
  > fresh install leaves them. Calls whose owner the host did not tell us are left alone too,
302
299
  > 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.
300
+ > The installer writes the six
301
+ > role prompts, which name `read` / `grep` / `shell` instead of the removed aliases.
307
302
 
308
303
  > **On OpenCode 2.x, interactive browsing is the host's, and it stays ours.** The
309
304
  > desktop renders a browser in its side panel, and that panel attaches to the
310
305
  > server's own browser service — a plugin cannot register a page of its own into it
311
306
  > (forensics: `docs/research/browser-pane.md`). So the three roles with network
312
307
  > 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
308
+ > — the self-built `tm_browser` was removed in 1.7.0, so the host's catalog is the
309
+ > only browser. On a host with no native browser catalog (CLI, standalone) there is
310
+ > now no browser at all, and the agent reports that gap rather than simulating one.
311
+ > Handing browsing over did not mean handing
315
312
  > governance over: `permission.evaluate` was observed NOT firing for `browser_*`, so
316
313
  > our gate sits on `tool.execute.before` instead — it classifies the URL of every
317
314
  > navigate/open, the path of every `browser_preview`, and every browser URL named
@@ -323,7 +320,7 @@ through with `tm_fetch` when it genuinely needs the payload.
323
320
  > leaked past the refusal). One exception to the offload rule lives here: a
324
321
  > `browser_snapshot` is an addressing table, not a document, so it is CAPPED (every
325
322
  > `[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
323
+ > default 1 200) rather than replaced by a
327
324
  > handle. Measured on a 261-ref page: a head cut keeps 118 refs, this keeps 261 of
328
325
  > them using 1 044 of 11 326 tokens.
329
326
 
@@ -332,8 +329,8 @@ through with `tm_fetch` when it genuinely needs the payload.
332
329
  > It doubles as the fallback chain: when a tool errors (no browser on this
333
330
  > host, blocked host), the agent says so and drops to the next rung — and
334
331
  > 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
332
+ > One stated exception: on the **web** channel `tm_search` / `tm_webfetch` come
333
+ > first, because that path is the only one carrying the
337
334
  > domain allowlist, the per-request confirmation dialog and the R6 red lines.
338
335
 
339
336
  > **What the host UI cannot show you.** OpenCode renders a *plugin* tool call
@@ -391,8 +388,8 @@ using an HMAC-signed, run-scoped, expiring handle — and on a JSON handle it
391
388
  can ask for a `fields` dot-path projection instead (a deliberately small jq
392
389
  subset: `items[].name`, `[].stargazers_count`), so a big API dump narrows to
393
390
  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.
391
+ The host's own `shell` runs read-only probes under its permission rules, and failures
392
+ come back as structured errors instead of raw dumps.
396
393
 
397
394
  ### Session + project + global memory (tm_memory)
398
395
 
@@ -452,49 +449,28 @@ Two more channels complete the surface:
452
449
  - `tm_webfetch` — a known URL, one governed GET. Search-engine pages it
453
450
  fetches are auto-extracted to hit lists too. JSON endpoints like
454
451
  `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.
452
+ - **Interactive browsing is the host's own `browser_*` tools** (`browser_tabs_open`
453
+ · `browser_navigate` · `browser_snapshot` · `browser_click` · `browser_evaluate`
454
+ …). The self-built `tm_browser` was **removed in 1.7.0** (a breaking change):
455
+ the desktop renders a browser in its side panel and that panel attaches to the
456
+ server's own browser service, so a plugin cannot register a page of its own
457
+ (forensics: `docs/research/browser-pane.md`). The host catalog is policed by
458
+ `src/host/v2-browser-gate.ts` at `execute.before` — the URL of every navigate/open,
459
+ the path of every `browser_preview`, and every browser URL named inside an
460
+ `execute` program are classified against the address red lines and the env-file
461
+ rule, and a refused call the host runs anyway has its page replaced with the same
462
+ refusal, so no out-of-policy content reaches the context, the store or the
463
+ trajectory. Calling convention differs from the deleted tool: `evaluate` takes
464
+ `{tabID, script}` where `script` is an **expression** (not a function source) and
465
+ the return value must be a scalar you `JSON.stringify` yourself; snapshot tokens
466
+ read `@e8 [link]`. **Known cost, accepted:** on a host with no native browser
467
+ catalog (CLI / standalone) there is now no browser at all — the agent reports that
468
+ gap rather than simulating one.
493
469
 
494
470
  When a fetch still returns **403 after the real-Chrome headers**, the error
495
471
  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
472
+ real browser passes — the agent is told to open that URL with the host's
473
+ native `browser_navigate` (and read it with `browser_snapshot`). Search hit lists also
498
474
  filter known noise: engine-internal wrappers (`so.com/link?`, `ai.so.com`)
499
475
  and same-name-different-site domains (`maimai.cn` 脉脉 vs the maimai DX
500
476
  game) never ride along — extend the hit blacklist with `TM_HIT_BLACKLIST`.
@@ -512,9 +488,9 @@ mzh.moegirl.org.cn, space.bilibili.com — is covered):
512
488
  `TM_WEBFETCH_ALLOWED_DOMAINS` (`"*"` opens every host; a custom list REPLACES
513
489
  the seed, so keep the engine hosts or `tm_search` loses its targets). Architect /
514
490
  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.
491
+ reported gap, never simulated. The tester carries the host's native `browser_*`
492
+ tools ONLY, for governed UI verification of the project (local dev servers,
493
+ preview routes); open web fetching stays with the two network roles.
518
494
 
519
495
  **Out-of-allowlist targets are a gate, not a wall.** When a fetch / search /
520
496
  browser-open points at a host outside the allowlist, the tool hands the URL
@@ -527,6 +503,12 @@ remain hard-rejected with no dialog — R6 red lines are never consentable.
527
503
 
528
504
  ### Security: the R6 + R2 approval gate
529
505
 
506
+ **On OpenCode 2.x, R6 is armed by default.** A Team role's native `read` of a `.env` is denied
507
+ outright — no consent path — and a shell command that reads the environment goes to the host's own
508
+ permission prompt (a plugin cannot raise a dialog on 2.x, so the shell face is the host's `ask`
509
+ while the env-file face is a hard `deny`). Turn it off with the plugin option
510
+ `envProtect: false` or with `TM_ENV_PROTECT=off`.
511
+
530
512
  **R6 environment protection.** With TeamMode active, the model cannot read
531
513
  environment variables silently. Env reads (`printenv`, `env`, `Get-ChildItem
532
514
  env:`, …) and env files (`.env`, shell rc) route through OpenCode's official
@@ -605,12 +587,18 @@ The plugin injects everything at startup — no agent files to copy.
605
587
 
606
588
  ```jsonc
607
589
  {
608
- "plugin": [
609
- ["@te-river/opencode-team-mode@latest", { "defaultAgent": false }]
590
+ "plugins": [
591
+ { "package": "@te-river/opencode-team-mode@latest", "options": { "defaultAgent": false } }
610
592
  ]
611
593
  }
612
594
  ```
613
595
 
596
+ Note the shape: on 2.x a plugin entry is either a string or an **object** with `package` and
597
+ `options`. The 1.x tuple form `["@te-river/opencode-team-mode@latest", { … }]` is rejected
598
+ (`path=$.plugins.1 kind=invalid`, measured 2026-10-06). Options do arrive — with the object
599
+ form the boot row reads `board_ttl_days=7` for `{ "ttlDays": 7 }`, so this is measured, not
600
+ documented-and-hoped.
601
+
614
602
  Your own agents named `team` / `architect` / … always take precedence; the
615
603
  plugin never clobbers user definitions. See [Customization](#-customization)
616
604
  for overrides, extra agents and disabling roles.
@@ -620,11 +608,11 @@ for overrides, extra agents and disabling roles.
620
608
  | Env var | Default | Purpose |
621
609
  |---|---|---|
622
610
  | `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 |
611
+ | `TM_R6_FINE_ASK` | classifier | The command line is judged per-call by the host's `permission.evaluate` hook, so an ordinary `git status` asks nothing and an env dump still does. `off` falls back to asking about EVERY shell command — which is also what happens automatically on a host that does not expose the hook, and the boot note says which of the two caused it |
624
612
  | `TM_V2_CODEMODE` | catalog (opt-in `direct`) — **v2 only** | How our tools reach the model. OpenCode 2.x decides tool visibility with `options.codemode`. We used to send `codemode:false` by default and claim direct delivery — **a live 2.0.16 desktop session disproved it**: with the flag sent, every `tm_*` still arrived inside the host's Code Mode catalog ("They cannot be called directly…"), and the model's own callable list was the nine native tools. So the default now sends nothing, and `direct` is an experiment for a build that honours it. The observable truth is `tools_in_request` in the shutdown record (rendered by `tm_stats` as `请求内实际可见=…`), never the flag we sent |
625
613
  | `TM_LEDGER_MAX_ITEMS` | 200 | Ceiling on `tm_ledger` items per session. The host's `ctx.storage` has no TTL and no quota (measured), so the list refuses to grow past this instead of quietly dropping the oldest asks — and a refusal is something the lead can act on, while a silent truncation is a claim nobody can re-check |
626
614
  | `TM_V2_PROBE` | — (v2 only) | Path to a JSONL file where the surface probe records the host's real tool ids, permission action names and argument key names. Names and counts only — never a command line, path, URL or env value. It is how "does the host actually have X?" gets answered from the running build instead of from a doc; without it the same name sets still ride the trajectory so `tm_stats` can show them |
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 |
615
+ | `TM_PRIVATE_SPACE` | `deny` | What private space (loopback, RFC1918, ULA, CGNAT, `.localhost`) does through our tools: `allow` opens the class, `deny` refuses it. The default is `deny` because a 2.x plugin cannot raise a dialog — telling the agent to wait for a window that will never open is not a gate with a procedure — so the refusal prints both operator exits instead (`allow`, or that one hostname in `TM_WEBFETCH_ALLOWED_DOMAINS`). Never conflated with the FORBIDDEN ranges: cloud-metadata / link-local / reserved are refused under every setting and every allowlist, with no consent path |
628
616
  | `TM_V2_BROWSER_GATE` | on — **v2 only** | The gate over the host's `browser_*` catalog (URLs and preview paths at `execute.before`, plus the browser URLs named inside an `execute` program). `off` restores the host's ungoverned browsing; the boot line and `tm_stats` say which world is running |
629
617
  | `TM_NATIVE_REPORT_MAX_TOKENS` | 1600 — **v2 only** | The budget a report-shaped native result (anything carrying a Markdown table — `tm_stats` via Code Mode, a `tm_join` summary) keeps in context. Table lines and their headings win the space and the prose between them is what gets dropped, because a table with a hole in it is not a table; the full text still rides the handle |
630
618
  | `TM_NATIVE_SNAPSHOT_MAX_TOKENS` | 1200 — **v2 only** | The budget a native `browser_snapshot` / `browser_find` keeps in context. Addressing lines win it before static text does; past `budget × 4` the reply states how many ref lines did not fit, and kept + dropped always equals the total |
@@ -640,25 +628,27 @@ for overrides, extra agents and disabling roles.
640
628
  | `TM_BLACKBOARD_TTL` | `7` | store retention (days) |
641
629
  | `TM_BOARD_MAX_CHARS` | `200000` | tm_board_write: one board file's character cap — over it the write refuses (with a "split the topic" hint) instead of truncating a deliverable |
642
630
  | `TM_BOARD_MAX_FILES` | `200` | tm_board_write: markdown files allowed per session folder; the TTL sweeper is the only reclaim path, so the refusal names it and the `ttlDays` option |
643
- | `TM_BASH_READONLY_ALLOWED` | built-in table | tm_bash allowlist |
644
631
  | `TM_SEARCH_DEFAULT_ENGINE` | `auto` | tm_search engine when no `engine` arg is given (`auto` = classify + parallel fan-out + RRF fusion; any table name also pins a manual default) |
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) |
632
+ | `TM_WEBFETCH_ALLOWED_DOMAINS` | `"*"` | tm_webfetch / tm_search allowlist (`"*"` opens all; empty = deny all; a custom list REPLACES the seed — keep the engine hosts). `"*"` does **not** cover private space: loopback / RFC1918 / CGNAT / `.localhost` still need the dialog on every use, and non-routable ranges (169.254.0.0/16 metadata, 0.0.0.0/8, multicast, reserved, plus the IPv4-mapped and DNS64 spellings of the same target) are a hard red line no setting can open |
633
+ | `TM_WEB_CACHE_TTL_SEC` | `300` | how long a governed fetch may re-serve the same URL (0 = off). One store shared by tm_webfetch / tm_search; entries are hash-named (a token-bearing query never hits disk) and are only read or written for hops the STATIC allowlist admitted — dialog consent stays per-request, and a re-served page says 缓存命中 |
655
634
  | `TM_SEARCH_WEIGHTS` | unset | per-engine fusion weight overrides, e.g. `bing=0.3,hn=0.25`; anything unset keeps the built-in table |
656
635
  | `TM_SEARCH_RELEVANCE_FLOOR` | `0.35` | weight fraction kept by a hit that shares no query token with its title/snippet/host (demotes junk without deleting an engine) |
657
636
  | `TM_SEARCH_MAX_HITS` | `10` | hits kept per engine leg and in the fused list |
658
637
  | `TM_SEARCH_DISABLED_ENGINES` | unset | engines removed from the roster AND from every `auto` route (`sogou,baidu` style) |
659
638
  | `TM_BASH_TIMEOUT_PROBE_MS` | `60000` | ceiling forced onto a `timeout` the model set for a read-only probe command (0 disables) |
639
+ | `TM_PRUNE` | `on` | `off` stops context pruning entirely (settled message bodies stay in the request) |
640
+ | `TM_PRUNE_AT_PERCENT` | `70` | prune once the request reaches this share of the model's window; derived from `limit.context`, never a fixed token count. **Clamped to 40–95** — a lower value is silently raised to 40, so a reproduction that sets 5 is really testing 40 |
641
+ | `TM_PRUNE_KEEP_TAIL_PERCENT` | `40` | share of the window kept verbatim at the tail; the newest message is always kept |
642
+ | `TM_RETRY` | `on` | `off` stops the retry governor (no backoff directive, no cooldown) |
643
+ | `TM_RETRY_BASE_MS` | `5000` | first backoff after a quota/rate-limit error; doubles per consecutive error |
644
+ | `TM_RETRY_MAX_MS` | `60000` | ceiling for that backoff |
645
+ | `TM_RETRY_JITTER` | `0.3` | ± fraction applied to the wait, so parallel sessions do not re-fire together |
646
+ | `TM_RETRY_BREAK_AFTER` | `5` | consecutive errors before the cooldown starts |
647
+ | `TM_RETRY_COOLDOWN_MS` | `60000` | how long new sub-agent dispatches are refused once the breaker trips |
648
+ | `TM_SPLIT_ADVICE` | `on` | `off` stops the split hint (the prompt discipline stays) |
649
+ | `TM_SPLIT_BRIEF_TOKENS` | `4000` | a dispatch brief above this size earns a split hint on the next request. **Clamped to ≥200** — a lower value is silently raised, so a reproduction that sets 50 is really testing 200 |
650
+ | `TM_SPLIT_MAX_CRITERIA` | `3` | the prompt's own rule: more acceptance criteria than this is a signal to split |
660
651
  | `TM_BASH_TIMEOUT_MAX_MS` | `0` | optional global ceiling for every other bash command — off by default so a real build keeps the timeout it asked for |
661
- | `TM_PTY_MAX` | `4` | concurrent `tm_pty` terminal sessions this plugin may keep running at once |
662
652
  | `TM_JOIN_MAX_WAIT_MS` | `60000` | ceiling on `tm_join { waitMs }`. Was 300 000, and a lead parked in it twice in a row (19 min of nothing) while its children worked — waiting is not parallelism, so the default now says "check, then work". A second consecutive wait after nothing settled is cut to 10 s and answered with what to do instead |
663
653
  | `TM_STORE_RECLAIM` | `on` | at boot, reclaim what an upgrade left behind: a per-workspace store shard idle past the TTL, and the pre-shard `blackboard/` + `trajectory/` under the temp-dir fallback (503 MB of expired runs on one real machine, with no sweeper pointed at them any more). Only TTL-expired entries are ever removed — a fresh run dir survives, because a session started before the upgrade may still be writing there. `off` leaves the disk exactly as found (the test runner sets it) |
664
654
  | `TM_TASK_OFFLOAD` | `on` | keep the HOST's background sub-agent inside the context budget: when `task { background: true }` finishes, the host injects the child's full reply into your session; this replaces an oversized body with a preview + a `tm_fetch`-style pointer (`tm_join { ids: [...] }`), touching ONLY synthetic parts that carry the host's own `<task id=… state="completed">` envelope and exceed the text offload threshold. Nothing is copied to disk — the text stays where it was written (the child session). `off` restores the host's verbatim injection. **On OpenCode 2.x this half does not apply**: a v2 plugin is never handed the injected message before it is persisted, and we deliberately do NOT rewrite outgoing messages (a wrong guess about that layer's shape deletes evidence silently — the v1 `experimental.chat.messages.transform` path is left unimplemented on purpose). The compensation there is contractual, not a rewrite: the child's oversized deliverable goes to the blackboard file and its reply carries the path, so the lead reads the summary and pulls the whole thing with `tm_join` |
@@ -666,21 +656,17 @@ for overrides, extra agents and disabling roles.
666
656
  | `TM_AGENT_TEMPERATURE` | `off` | `on` applies a per-role sampling table (architect 0.35 / researcher 0.3 / reviewer 0.1 / rest 0.2) via `chat.params`; or give it `reviewer=0.05;team=0.4`. Off = the documented "all agents at 0.2" invariant stands |
667
657
  | `TM_COMPACTION_CONTEXT` | `on` | on the host's pre-compaction hook, add the must-survive list (reply skeleton, offload handles, open sub-agent session ids, provenance, board paths). Additive — the host's own summarizer prompt is never replaced |
668
658
  | `TM_COMPACTION_AUTOCONTINUE` | `on` | `off` stops the host from silently resuming the turn after a compaction, so a human re-reads state first |
659
+ | `TM_COMPACT_TRIGGER` | `on` | Team's own early compaction: `off` hands the timing back to the host entirely |
660
+ | `TM_COMPACT_AT_PERCENT` | `75` | usage percentage of the model's window at which Team submits a compaction via `ctx.session.compact` (clamped 5–95). The usage number comes from the host's `session.usage.updated` event, the denominator from `ctx.model.list()`'s `limit.context` |
661
+ | `TM_COMPACT_MIN_MS` | `60000` | floor between two compaction admissions for one session (capped 600000), so a ratio that does not drop cannot become a compaction loop |
669
662
  | `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
663
  | `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
664
  | `TM_MEMORY_GLOBAL_DIR` | `~/.opencode-team/memories/global/` | tm_memory GLOBAL tier store |
673
665
  | `TM_MEMORY_SESSION_TTL_MIN` | `240` | session-tier entry TTL (lazy + boot sweep) |
674
666
  | `TM_MEMORY_MAX_ENTRIES` | `200` | per-scope entry cap; over it `add` fails on purpose — run `compact` |
675
667
  | `TM_MEMORY_STALE_DAYS` | `30` | age after which search hits are tagged `[stale Nd]` (`0` disables) |
676
668
  | `TM_MEMORY_SESSION_PERSIST` | — (ephemeral) | `1` also writes session entries under `memories/sessions/<sid>/` |
677
669
  | `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
670
 
685
671
  ---
686
672
 
@@ -745,15 +731,27 @@ not the rule: a deliverable over ~50 lines goes to ONE named board file under
745
731
 
746
732
  **Disable one:** `"researcher": { "disable": true }`.
747
733
 
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).
734
+ **Board retention** via the plugin entry's `options`:
735
+
736
+ ```jsonc
737
+ {
738
+ "plugins": [
739
+ { "package": "@te-river/opencode-team-mode@latest", "options": { "ttlDays": 7 } }
740
+ ]
741
+ }
742
+ ```
743
+
744
+ Valid range (0, 365], invalid values fall back to 5. Measured 2026-10-06: with this shape the
745
+ boot row reads `board_ttl_days=9` for `ttlDays: 9`. (The 1.x tuple form is rejected on 2.x:
746
+ `path=$.plugins.1 kind=invalid`.)
749
747
 
750
748
  ---
751
749
 
752
750
  ## ❓ FAQ
753
751
 
754
752
  **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
753
+ The opposite is the point. Offload + 80-token previews + the host's Code Mode
754
+ `execute` exist because a five-agent pipeline naively bolted onto one context window
757
755
  *would* eat your tokens. The governance is the token-saver.
758
756
 
759
757
  **Is the web access safe?**
@@ -779,8 +777,9 @@ here fails the test suite before it ever reaches you.
779
777
 
780
778
  **Does it work in the CLI (TUI), or only Desktop?**
781
779
  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.
780
+ the whole workflow are host-agnostic. Interactive browsing is the host's own
781
+ `browser_*` catalog, so a host with no native browser (CLI / standalone) has
782
+ none — the agent reports that gap rather than simulating one.
784
783
 
785
784
  **What happens if I don't answer a confirmation dialog?**
786
785
  It auto-rejects after `TM_ASK_TIMEOUT_MIN` (default 1). The plugin never
@@ -790,7 +789,7 @@ self-approves — the only side it can take is yours or nobody's.
790
789
 
791
790
  ## 🗑️ Uninstall
792
791
 
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.
792
+ 1. Remove the entry from the `"plugins"` array in your config file; if you installed on 2.x, delete the generated `~/.config/opencode/agents/*.md` and `commands/team-*.md` too, and any `default_agent: "Team"` you no longer want.
794
793
  2. Delete the cache dir (table in [Install](#-read-this-once-save-yourself-an-hour-later)) if you want the disk space back.
795
794
  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
795
 
@@ -803,28 +802,28 @@ No DLLs were harmed. Nothing was written to your working tree.
803
802
  ```
804
803
  opencode-team-mode/
805
804
  ├── src/
806
- │ ├── index.ts ← Plugin entry (config + R6 guard + approval gate + tool segment)
805
+ │ ├── index.ts ← Plugin entry ({id, setup} — v2 only)
807
806
  │ ├── agents.ts ← Agent structure (modes, colors, temperatures, whitelist matrix)
808
807
  │ ├── prompts/ ← Agent prompts (lead / specialists / shared) — pinned by tests
809
808
  │ ├── commands.ts ← Slash command definitions
810
809
  │ ├── blackboard.ts ← Shared blackboard + TTL sweeper
811
810
  │ ├── envprotect.ts ← R6 facade (patterns / classifiers / gate predicates / hook)
812
- │ ├── approval-gate.ts ← Unified approval gate (dialog timeout auto-reject)
811
+ │ ├── identity.ts ← Agent-name identity (case-insensitive lead match)
813
812
  │ ├── 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)
813
+ │ │ webfetch / search / memory / browser / shell-bridge / board / ledger
814
+ │ ├── host/ ← v2 personality (setup / guard / offload / session / events / …)
815
+ │ └── types.ts ← Loader-contract types
816
+ ├── docs/installation.md ← Historical 1.18.x guide (no longer supported)
817
817
  ├── docs/installation-v2.md ← The 2.x install guide (roles arrive as generated config files)
818
818
  ├── scripts/ ← One-line installers (bash / PowerShell)
819
819
  ├── pt07/ ← PT-07 baseline suite (seeded A/B token measurement)
820
820
  └── README.*.md ← You are here (twice)
821
821
  ```
822
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.
823
+ The loader calls `setup(ctx)` once: it registers the `tm_*` tools, installs the
824
+ R6/address guard on `permission.evaluate`, attaches the JIT offload on
825
+ `tool.execute.after`, and publishes the blackboard note. User-defined agents with
826
+ the same name always win — the plugin never clobbers.
828
827
 
829
828
  ---
830
829
 
@@ -846,8 +845,8 @@ are intentionally loose but not psychic).
846
845
  ## 🔗 Links
847
846
 
848
847
  - [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
848
+ - [Installation guide, **OpenCode 2.x**](./docs/installation-v2.md) — the supported install: a 2.x plugin cannot create an agent, so the six roles and six commands are generated into the config directory
849
+ - [Installation guide, 1.18.x](./docs/installation.md) — **historical only**: the 1.18.x personality was removed in the 1.7.0 line, so this page is kept for reference and is not a working install path
851
850
  - [OpenCode Desktop](https://opencode.ai) — Official website & download
852
851
  - [OpenCode Docs](https://opencode.ai/docs) — Configuration & plugin documentation
853
852
  - [OpenCode Plugin API](https://opencode.ai/docs/plugins) — Build your own plugins