@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
@@ -0,0 +1,315 @@
1
+ # The v2 port, in full — evidence, history and the reasoning behind each rule
2
+
3
+ AGENTS.md keeps the **rules** and points here for the **evidence and the story**: what was
4
+ measured, on which host build, which claim was later falsified and by what, and why a
5
+ decision was taken the way it was. This split exists because AGENTS.md is loaded into every
6
+ session — prose there is paid for per request — while this file costs nothing until someone
7
+ reads it, which is the same rule the prompts follow (see "subtraction philosophy" in
8
+ AGENTS.md).
9
+
10
+ Nothing was rewritten when it moved here: the section below is byte-exact from AGENTS.md as
11
+ of 2026-09-26. New findings append to the section that owns them, with a date and the
12
+ evidence tag (`[B]` binary read, `[L]` live host, `[R]` our own measured code, `[D]` docs).
13
+ Related: `docs/research/plugin-loader-contract.md` (how the host loads a plugin),
14
+ `docs/research/host-subagent-injection.md` (the background child's three moments),
15
+ `docs/research/v2-builtin-tools.md`, `docs/research/browser-pane.md`,
16
+ `docs/research/agent-data-exchange.md`, and `docs/installation-v2.md` for the install flow.
17
+
18
+ ## The v2 delta (2026-09-25, in force)
19
+
20
+
21
+ The goals above were written against OpenCode 1.18.x. These are the places where
22
+ the 2.x port changes the business claim rather than the implementation — read
23
+ this before treating any sentence above as platform-neutral.
24
+
25
+ - **Everything the plugin changes is Team-scoped (user requirement, 2026-09-25).**
26
+ Every v2 hook fires for EVERY session on the host, so `src/host/v2-scope.ts` is the
27
+ one answer each layer asks before it writes: the per-role tool trim, `temperature`
28
+ 0.2, the board note, the compaction survival list, the native-result offload, the
29
+ R6/egress permission strictening, and the forced `background: true` all no-op for
30
+ `build`, `plan`, or any agent that is not one of our six — those modes must look
31
+ exactly like a freshly installed OpenCode. A session also becomes "ours" when one of
32
+ our own tools serves it (`bindV2Tool`'s `onCall` → `learn`), because the host does not
33
+ promise `agent` on every event. The third verdict, `unknown`, is deliberately NOT
34
+ treated as ours: isolation outranks coverage, so an unresolved call is left completely
35
+ alone — and it is COUNTED (`scope_unknown`) and printed by `tm_stats` with the sentence
36
+ "没资格治理", because a governance layer that silently stopped applying after a host
37
+ upgrade is the overstated claim this product exists to refuse. The consequence is said
38
+ out loud in the boot notes rather than hidden: outside Team the red lines we inject
39
+ (metadata/private addresses, R6 classification) do NOT apply either — that floor is the
40
+ host's or the user's own config's job. (v1 cannot be scoped this way: its `config` hook
41
+ and bash escalation sit in the FROZEN personality.)
42
+ - **Goal 1's injection is gone on v2.** `AgentEditor` has no `add`, and
43
+ `ctx.command.transform.add` was measured NOT to reach the UI, so the six roles
44
+ and the six `/team-*` commands are config files —
45
+ `~/.config/opencode/agents/*.md` and `commands/*.md`, written by the installer
46
+ and projected from `dist/` by `scripts/gen-v2-config.mjs` — never hand-copied,
47
+ or the prompts drift. The
48
+ plugin still owns the permission matrix (it merges its triples onto whatever
49
+ the config declares) and still ATTEMPTS the Team-default promotion on every boot
50
+ -- but `editor.default("team")` is measurably a no-op against a live host (see
51
+ the v2-default-agent gap below), so the requirement is satisfied by the
52
+ INSTALLER writing `default_agent: "team"`, not by this call.
53
+ - **Goal 5's "ask the user" does not exist on v2.** A plugin cannot raise the
54
+ host's official dialog (live-probed: declaring `options.permission` on our own
55
+ tool triggered no evaluation and no prompt). Governed calls therefore **fail
56
+ closed** with a v2-worded refusal instead of asking, and the only dialog left
57
+ is one the host raises on its own (`effect:"ask"` from a permission rule or
58
+ `permission.hook("evaluate")`). Anything that reads the v1 ask path as a model
59
+ for v2 is wrong.
60
+ - **Goal 4 is now the hard floor, and it has to cover the host's own tools.**
61
+ `tool.hook("execute.after")` takes a mutable `result` (docs example reassigns
62
+ it; a live probe confirmed the write lands), so offload + ≤80-token preview +
63
+ HMAC handle must extend to native `read`/`grep`/`shell`/`webfetch`. A promise
64
+ that only holds while the model happens to pick `tm_*` is not a promise.
65
+ **Both halves are now in:** `src/host/v2-session.ts` removes every tool a role
66
+ is denied from the assembled request (and the whole `browser_*` catalog where
67
+ `tm_browser` is denied), restores the 0.2 temperature, carries the resolved
68
+ board root to the lead, and starts the blackboard sweeper v2 silently lacked;
69
+ `src/host/v2-offload.ts` governs the NATIVE results through
70
+ `tool.hook("execute.after")` — measured on a live host: native `shell` 12 902
71
+ tokens arriving as a 78-token preview, `execute` 15 000 as 58.
72
+ **The governed name list is now every tool a Team role can call** (user
73
+ requirement #2: coverage may not depend on which tool the model picked):
74
+ `read grep glob shell bash webfetch websearch execute edit write patch question
75
+ subagent` plus the whole `browser_*` namespace matched BY PREFIX, because the host
76
+ has 45 of them and a 45-name list goes stale on the 46th without notice. Safety did
77
+ not move with the names — the decision to rewrite is per payload (an unrecognised
78
+ shape is left byte-exact), which is what the closed name list was really protecting.
79
+ And coverage is now a NUMBER: the report separates `ours` (every call resolved to a
80
+ Team session), `seen` (on the governed surface), `unmatched` (ours, but a tool we do
81
+ not touch) and `offloaded`; `tm_stats` prints them, so "most calls are JIT-governed"
82
+ can be checked and can also admit what it missed.
83
+ - **Done (1.7.0 line): `tm_read` / `tm_grep` / `tm_bash` retired on v2 only.**
84
+ The ordering AGENTS.md demanded held: governance moved down FIRST
85
+ (`permission.hook("evaluate")` for the address + R6 red lines,
86
+ `execute.after` for the offload), and only then did the aliases go. Where the
87
+ third piece of v1's P2 went: the host has its own `external_directory`
88
+ permission action, observed live answering `effect:"ask"` with a real
89
+ `permission.asked` behind it — so the native ladder is scoped by a dialog
90
+ rather than by our hard throw, which is stricter, not looser. The retirement
91
+ has TWO halves and both are pinned by `test-v2-adapter` group 3: the trio is
92
+ not registered and gets no permission triples (`V1_ONLY_TOOLS`, now also the
93
+ `mergeTriples` reclaim set, so a stale `tm_read` rule from an earlier boot is
94
+ collected), AND v1's matrix denies native `read`/grep/glob for every role —
95
+ projecting those denies would have left a v2 role with NO file access at all,
96
+ which is why `V2_LADDER_ACTIONS` exempts them in both the triple translation
97
+ and `toolsToRemove()`. v1 keeps all three because it cannot rewrite a tool
98
+ result at all. The two personalities now ship different tool surfaces, and
99
+ the prompt fork is real: `V2_TEXT` in `gen-v2-config.mjs` rewrites every
100
+ sentence that named the trio or called the shell `bash` (a key that stops
101
+ matching throws the build), which is why the v2 config files say `read` /
102
+ `grep` / `shell`.
103
+ - **The bloat number, and what it is NOT.** Summing the thirteen `tm_*`
104
+ definitions through our own `estimateTokens` gives **9 528 tokens**
105
+ (descriptions 6 639 + schemas 2 889) — that is the size of OUR definition set,
106
+ measured offline against `dist/`. **It is not a per-request cost, and a live
107
+ 2.0.16 session says so:** the `team` agent's assembled request carried
108
+ **6 tools** (`edit`/`question`/`shell`/`subagent`/`write`/`execute`) and
109
+ **zero `tm_*`**, while the model's 61 calls in that session went to
110
+ `shell`(42)/`execute`(14)/`edit`(4)/`write`(1). So either plugin tools are
111
+ delivered only through the Code Mode catalog (the way `browser_*` is) or the v2
112
+ **RESOLVED (2026-09-25, read out of the 2.0.16 binary): the catalog explanation is
113
+ the right one, and it is our own doing.** Visibility is decided by
114
+ `options.codemode` — a tool whose value is not `false` is offered ONLY inside the
115
+ Code Mode catalog, where the host keeps `≤120 chars` of the description's FIRST
116
+ line under a ~2 000-token budget. `bindV2Tool` never sent `options`, so every
117
+ `tm_*` we register was catalog-only from the start: the tools WERE live (callable
118
+ from inside `execute`), and almost none of the governance text written for them
119
+ ever reached the model. `TM_V2_CODEMODE=direct` now sends the flag, and the cost is
120
+ MEASURED per role through our own `estimateTokens` (definitions + schemas, after
121
+ the request-layer trim): architect / implementer / reviewer **2 660** tokens,
122
+ tester **5 448**, researcher **7 113**, team **8 515** — i.e. direct delivery is
123
+ affordable for the three build-class roles and expensive for the two network roles
124
+ plus the lead, where `tm_browser` alone contributes 2 788. **DECIDED (user,
125
+ 2026-09-25): slim first, then default to direct.** `tm_browser`'s description went
126
+ from a narrative of every lesson to what the model cannot recover from a result —
127
+ 729 tokens (its parameter table is the other 1 337, and those descriptors are
128
+ per-parameter truth, not prose), and with `tm_pty` retired too the measured cost is
129
+ now **2 660 for architect/implementer/reviewer, 4 726 tester, 6 391 researcher,
130
+ 7 366 lead**, which is what `direct` was supposed to buy per request. **FALSIFIED
131
+ 2026-09-26 by the user's own desktop session**: with `options.codemode:false` sent for
132
+ all ten tools, 2.0.16 still delivered every `tm_*` inside the host's Code Mode catalog
133
+ ("They cannot be called directly…"), and the model's own callable list was the nine
134
+ native tools. So the flag changes what WE send and nothing else, the default now sends
135
+ nothing (`TM_V2_CODEMODE=direct` is an opt-in experiment for a build that honours it),
136
+ and the outcome is an OBSERVATION — `tools_in_request`, derived from whether a `tm_*`
137
+ name ever appears in an assembled request's own tool map — printed by `tm_stats` beside
138
+ the sent flag. The token figures above stand as measurements of OUR definition set;
139
+ what turned out to be fiction was the claim that the model receives them.
140
+ The history did not disappear — it stayed in AGENTS.md and CHANGELOG.md
141
+ where it costs nothing per request, which is the rule this repo applies to prompts
142
+ generally (see the subtraction philosophy).
143
+ What the live session DID confirm: the per-role trim works (the native
144
+ read/grep/glob/list/webfetch/websearch/skill/patch the matrix denies were
145
+ gone from the request).
146
+ - **Goal 4 gains a third outcome besides "in context" and "offloaded": CAPPED.** A
147
+ live session caught the generic offload eating a report: a `tm_stats` answer called
148
+ through Code Mode arrived as the `execute` result at 2 917 tokens and came back as an
149
+ 80-token preview, so the lead spent the round paging a table back in and the user got
150
+ prose about numbers. Two payloads are structurally unreadable once previewed — a
151
+ browser snapshot (its `[ref=…]` lines ARE the next click's arguments) and a report
152
+ (a table with a row missing is not a smaller table, it is a broken one) — so
153
+ `capKeepingAddressing()` and `capKeepingTables()` keep the structural lines and drop
154
+ the prose instead, up to `budget × 4`, and say how much did not fit. `reportCapped` /
155
+ `capped` / `offloaded` are three separate counters in `tm_stats`, because they answer
156
+ three different questions about what entered the context.
157
+ - **The two personalities now ship different tool surfaces, and the prompt fork
158
+ has a home.** `V1_ONLY_TOOLS` (in `v2-permissions.ts`) is the single source for
159
+ what v2 does not register; `v2.ts` skips those when registering and
160
+ `gen-v2-config.mjs` drops their permission triples, because an `allow` for an
161
+ action the host never heard of claims a capability that does not exist. The
162
+ sentences that name them are rewritten by the `V2_TEXT` table in the same
163
+ generator -- exact strings, and **it throws if a key stops matching**, so
164
+ editing a prompt without updating the table fails the build instead of
165
+ silently shipping a v2 model a rule about a tool it cannot call. The lead's delegation
166
+ section is the SECOND instance of that bug class and is forked the same way: on v2
167
+ the host's tool is `subagent` (measured -- `execute.before {tool:"subagent"}` and
168
+ `permission.evaluate {action:"subagent"}`), background needs no operator flag
169
+ because the plugin forces it, so both the "pick a synchronous `task`" rule and the
170
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true` sentence are v1 text that would
171
+ have a v2 lead checking a switch that does not exist. This is the
172
+ mechanism the `tm_read`/`tm_grep`/`tm_bash` retirement (done, 1.7.0 line) and
173
+ the v2-only `tm_ledger` (also done — the generator throws on a key that stops
174
+ matching, which is what keeps a v2 prompt from naming a tool v2 does not have).
175
+ - **`tm_browser` is probe-gated, not decided.** The host ships 45 `browser_*`
176
+ tools and a `browser` deny rule with `resource:"*"` removes the whole catalog,
177
+ so the shape worth testing is "thin door over the host's own panel" via
178
+ `ctx.rpc("experimental.browser")` — whether a plugin can drive that
179
+ user-visible panel has never been verified live. If it cannot, playwright-core
180
+ stays and the tax is real. What is already known as lost either way: no
181
+ subresource policy on the native side, no headless ("screenshots require a
182
+ focused visible tab"), and no per-target consent.
183
+ - **`tm_search` survives because native `websearch` needs a key.** All four
184
+ providers (Exa / Firecrawl / Parallel / Tavily) require an API key or
185
+ `/connect`, while the governed front's whole premise is zero-key and
186
+ CN-reachable. **The hole this section recorded is now closed:** native
187
+ `webfetch` had no egress red line, so a Build-class agent could fetch
188
+ `169.254.169.254` past `checkWebUrl` -- `src/host/v2-guard.ts` now asks the
189
+ address question at `permission.hook("evaluate")` regardless of R6 (metadata /
190
+ link-local / reserved = deny with no consent path, IPv4-mapped and DNS64
191
+ carriers unwrapped first, private = the host's own ask, env-file URL = deny),
192
+ and it only ever gets STRICTER than whatever the host or a user rule already
193
+ chose. The same hook carries R6's per-command classification, and it is now IN
194
+ CHARGE: a live `--standalone` run recorded `{action:"shell", resourceCount:1}`
195
+ reaching `permission.evaluate` for a real `git status --short`, which is the
196
+ proof the coarse config-level `shell -> ask` was being kept for. So the blanket
197
+ escalation is the FALLBACK, reached either by `TM_R6_FINE_ASK=off` or by the host
198
+ not exposing `permission.hook` at all — and the boot note names WHICH of the two
199
+ applies, because two causes sharing one message is how a fallback gets mistaken
200
+ for a setting. The hook keeps counting what it sees (`shellMatched`) so the
201
+ classifier's own hit rate stays measurable rather than assumed.
202
+ - **What the Team agent is actually OFFERED on v2 (measured, not documented).**
203
+ Six tools in the request: `edit execute question shell subagent write`. That list
204
+ is the ground truth for three otherwise-guessed things. ① `read`, `grep`, `glob`,
205
+ `webfetch`, `websearch`, `skill`, `patch` are absent **because our own matrix
206
+ denies them and the request layer deletes denied tools** — the host has those
207
+ actions (it names them in `permission.evaluate` and in our triples), so retiring
208
+ `tm_read`/`tm_grep` is a permission flip plus governance, not a search for a
209
+ missing tool; and inside `execute` (Code Mode) the native names genuinely do not
210
+ exist — `tools.read`/`tools.shell`/`tools.webfetch` return `Unknown tool` while
211
+ `typeof` reports `"function"` for all three, so a probe that trusts `typeof` gets
212
+ a false positive. ② `todowrite` is gone, and the LEDGER rule now has a home anyway:
213
+ `tm_ledger` keeps the lead's list in the host's own `ctx.storage`, and
214
+ `tm_join`'s goal tripwire reads it (an empty list answers `ledger_empty`, never a
215
+ silent pass).
216
+ Measured facts about that domain (live 2.0.16 probe, `docs/research/agent-data-exchange.md`): it is shared across sessions, agents AND projects — the only namespace is the plugin id — which is why the session id is part of the key and a call without one is refused; and it has no TTL and no quota, which is why `LEDGER_MAX_ITEMS` (default 200, `TM_LEDGER_MAX_ITEMS`) REFUSES rather than truncating — silently dropping the oldest asks would be the same overstated claim wearing a new uniform. ③ `execute.after` sees a native `shell` result with keys
217
+ `content / metadata / output`, which is the precondition for putting JIT offload
218
+ over the native tools (#5) — without that observation the retirement of
219
+ `tm_bash` would have been an assumption.
220
+ - **Goal 5 is REPLACED on v2: no domain gate at all, address red line only.** The
221
+ user's instruction (2026-09-25) is that nothing on the network may be blocked
222
+ except sensitive and internal addresses. On v1 the 22-host seed list was livable
223
+ because the plugin could raise the host's official per-request dialog; on v2 it
224
+ cannot raise one, so an allowlist became a set of pages nobody can approve -- a
225
+ gate with no door. v2 therefore resolves its tool config from a COPY of the env
226
+ with `TM_WEBFETCH_ALLOWED_DOMAINS` defaulting to `"*"` (an explicit user value
227
+ always wins; `createTmTools` gained an `env` option precisely so v1 keeps its
228
+ shipped default in the same process). What still holds, hardest: `checkWebUrl`'s
229
+ address policy -- link-local/metadata/reserved ranges are a deny no config can
230
+ open, and private space (loopback, RFC1918, CGNAT, `.localhost`) stays refused
231
+ through our tools, which have no dialog to ask with. Verified against the built
232
+ runtime: `https://example.com` (in no seed) returns the page, while
233
+ `169.254.169.254`, `192.168.1.1` and `localhost:3000` are refused before any
234
+ request. The refusal wording is pinned too: a v2 refusal may not promise "只能逐次
235
+ 经用户批准" without also saying the host cannot open that dialog.
236
+ **What this does NOT buy: per-target visibility inside the native browser.** The
237
+ Team's direct surface is six tools (`edit execute question shell subagent write`),
238
+ so `tools.browser.*` output reaches context ONLY as the aggregate return of one
239
+ Code Mode program -- which is why `execute` is on the governed-tool list:
240
+ measured live, a 15,000-token program return arrived as `offloaded:true /
241
+ preview_tokens:58` + a `tm_fetch` handle. Governing the native browser means
242
+ governing that door.
243
+ - **Goal 6 gains one more instance.** `inputSchemaFor` now reports `source`,
244
+ because a table *derived by regex from a descriptor string* and a table
245
+ *translated by zod* are both `exact` but are not the same claim — the boot log
246
+ names which tools got which.
247
+ - **The "all agents 0.2" invariant cannot live in agent config on v2**
248
+ (`temperature` is a documented legacy field and the runner "preserves these
249
+ values but does not yet send them with model requests"); it must go through
250
+ `session.hook("context").options`. Background subagents, by contrast, are
251
+ native on v2 (`subagent {background:true}`, default nesting depth one = T3's
252
+ "only the lead dispatches" for free), so the
253
+ `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` env hack in the installers is
254
+ v1-only and must fork rather than delete.
255
+ - **The installer's v2 job is TWO things, and the plugin half of it is one config
256
+ key.** Reverse-engineered from the host binary and confirmed on live 2.0.16/2.0.18
257
+ (`docs/research/plugin-loader-contract.md` is the evidence page): the key is
258
+ **`plugins` (plural)** — a 2.x host never reads the 1.18.x singular `plugin` key, which
259
+ is why an install that wrote there looked complete and did nothing; an entry is
260
+ **installed by the host itself at startup** (`Bun.add` inside `PluginModule.load`, then
261
+ `msg="loading plugin" id=… entrypoint=file:///…/.cache/opencode/npm/<pkg>@latest/<ts>/
262
+ node_modules/<pkg>/dist/index.js`), so `opencode plugin add` is a convenience that
263
+ writes the same key and v1's cache-purge / npm-re-resolve machinery is obsolete; a
264
+ **directory** target resolves only as `<dir>/index` (hence the root `index.js` this
265
+ package ships) and an unresolvable one is dropped with **no message at all**, so the
266
+ only proof of an install is that log line or our own `v2-boot` row; and
267
+ `plugin add` **refuses a path** ("Plugin target must be an npm registry package or Git
268
+ package specifier"), which is why local mode is a config edit. The second measured fact
269
+ is the one that bites: `opencode.json` and `opencode.jsonc` are **both parsed and
270
+ merged** while the host's dedupe matches only an identical string, so the same plugin
271
+ under two spellings or in both files **loads twice** (two personalities, the same hooks
272
+ bound twice) — which is exactly what the old 1.18.x "migrate `.json` forward to `.jsonc`"
273
+ rule produced, so that migration is gone and the installers now write one entry into
274
+ the `.jsonc` (user requirement: keep the file previous installs used) and RECLAIM their
275
+ own entry from the legacy `.json`. Both front-ends call one
276
+ `scripts/lib/config-surgery.cjs` so they cannot drift, and it matches Team entries
277
+ case- and hyphen-insensitively because a working tree is `Opencode-TeamMode`. What
278
+ nothing else can do remains: write `agents/*.md`, `commands/*.md`, and `default_agent`
279
+ (which must come last — a default naming a missing agent makes the host fall back to
280
+ `build` silently).
281
+ - **The host's own background children are collectable (`babfcb8`).** On v2 every
282
+ dispatch IS the host's `subagent` tool, and `tm_join` had no record of them, so it
283
+ answered 没有待收集的派发 about work the user could watch on screen and a bounded wait
284
+ could never end. `src/host/v2-subagent.ts` pairs the two seams the user's own exported
285
+ session shows (`docs/research/host-subagent-injection.md`): `execute.before` carries
286
+ `{agent, background, description}` and `execute.after` carries
287
+ `result.metadata.sessionID` + `status:"running"`, with the ack sentence as a second
288
+ source since no field shape survives an upgrade unchanged. Two honesty rules come with
289
+ it: the child's BODY is the host's injected message, which v2 never hands a plugin
290
+ before persisting, so a host child with nothing readable says where the text actually
291
+ arrives instead of printing a reply we never read; and settle provenance is printed —
292
+ the child's own `session.idle` is `event`, while "its parent went idle so the host has
293
+ collected it" is labelled 推定 (the timestamps prove the ordering, once). A synchronous
294
+ child has no id and registers nothing, which is the correct answer, not a failure.
295
+
296
+
297
+ ### 2026-09-26 (later the same day) — that BODY sentence was our decode miss `[L]`
298
+
299
+ The paragraph above says the child's body is something v2 never hands a plugin. It is true of
300
+ the INJECTION (there is still no v2 equivalent of `chat.message`, so the message cannot be
301
+ rewritten before it persists) and false of the BODY, which is readable: `ctx.session.context
302
+ ({sessionID})` answers with an array of flat `{id, time:{created}, text, type}` items, measured
303
+ against a real background child (`array(5)`, first item `type:"user"`). The bridge had been
304
+ calling that seam all along and handing the array to `lastAssistantMessage`, which returns no
305
+ text for a shape it does not recognise — so the call SUCCEEDED, the report was discarded, and the
306
+ user-visible sentence blamed the host. Flat items are normalised at the seam now
307
+ (`normaliseContextMessages`, no invented `time.completed`), the reply credits
308
+ `正文来源=ctx.session.context`, and the trajectory writes `child_body` / `child_body_missing`
309
+ with the seam name only.
310
+
311
+ Two adjacent findings from the same zero-token probe (`--standalone --model nope/nope`, a plugin
312
+ that only inspects its own ctx): the plugin ctx carries `permission.list/get/reply` and
313
+ `experimental.terminal.read`, all decoding `{sessionID}`; and `ctx.rpc` is a function of arity 1
314
+ whose convention has NOT been read. `permission.reply` remains uncalled by design — answering a
315
+ dialog for the user is self-allowing. Recorded in `docs/research/host-http-api.md` §4/§6.
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // The host resolves a plugin DIRECTORY as `<dir>/index.js` (measured on 2.0.16:
2
+ // `msg="loading plugin" … entrypoint=file:///…/team-mode/index.js`, while a directory that
3
+ // only declares package.json#exports is skipped with no error at all). This shim is
4
+ // therefore part of the contract, not a convenience: without it neither
5
+ // `plugins: ["…/opencode-team-mode"]` in a config nor a node_modules install under that
6
+ // key can load the plugin. The dual-personality shape itself lives in dist/index.js.
7
+ export { default } from "./dist/index.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@te-river/opencode-team-mode",
3
- "version": "1.5.15",
3
+ "version": "1.6.1",
4
4
  "description": "Team collaboration mode plugin for OpenCode Desktop — injects specialized agents (architect, reviewer, implementer, tester, researcher) and team workflow commands into your OpenCode workspace.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,6 +13,9 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "index.js",
17
+ "scripts/lib",
18
+ "scripts/gen-v2-config.mjs",
16
19
  "README.md",
17
20
  "README.zh-CN.md",
18
21
  "LICENSE",