@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.
- package/README.md +117 -118
- package/README.zh-CN.md +98 -100
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +89 -79
- package/dist/agents.js.map +1 -1
- package/dist/approval-gate.d.ts +8 -9
- package/dist/approval-gate.d.ts.map +1 -1
- package/dist/approval-gate.js +13 -14
- package/dist/approval-gate.js.map +1 -1
- package/dist/capabilities.d.ts +11 -37
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +11 -141
- package/dist/capabilities.js.map +1 -1
- package/dist/commands.js +1 -1
- package/dist/envprotect/hook.d.ts +12 -31
- package/dist/envprotect/hook.d.ts.map +1 -1
- package/dist/envprotect/hook.js +13 -91
- package/dist/envprotect/hook.js.map +1 -1
- package/dist/envprotect/patterns.d.ts +6 -4
- package/dist/envprotect/patterns.d.ts.map +1 -1
- package/dist/envprotect/patterns.js +6 -4
- package/dist/envprotect/patterns.js.map +1 -1
- package/dist/envprotect.d.ts +1 -1
- package/dist/envprotect.d.ts.map +1 -1
- package/dist/envprotect.js +1 -1
- package/dist/envprotect.js.map +1 -1
- package/dist/host/v2-browser-gate.d.ts +19 -1
- package/dist/host/v2-browser-gate.d.ts.map +1 -1
- package/dist/host/v2-browser-gate.js +47 -6
- package/dist/host/v2-browser-gate.js.map +1 -1
- package/dist/host/v2-capabilities.d.ts +33 -0
- package/dist/host/v2-capabilities.d.ts.map +1 -1
- package/dist/host/v2-capabilities.js +50 -1
- package/dist/host/v2-capabilities.js.map +1 -1
- package/dist/host/v2-compaction.d.ts +129 -0
- package/dist/host/v2-compaction.d.ts.map +1 -0
- package/dist/host/v2-compaction.js +346 -0
- package/dist/host/v2-compaction.js.map +1 -0
- package/dist/host/v2-default-agent.d.ts +37 -0
- package/dist/host/v2-default-agent.d.ts.map +1 -0
- package/dist/host/v2-default-agent.js +128 -0
- package/dist/host/v2-default-agent.js.map +1 -0
- package/dist/host/v2-events.d.ts +2 -0
- package/dist/host/v2-events.d.ts.map +1 -1
- package/dist/host/v2-events.js +27 -0
- package/dist/host/v2-events.js.map +1 -1
- package/dist/host/v2-guard.d.ts +178 -0
- package/dist/host/v2-guard.d.ts.map +1 -1
- package/dist/host/v2-guard.js +324 -8
- package/dist/host/v2-guard.js.map +1 -1
- package/dist/host/v2-offload.d.ts +1 -1
- package/dist/host/v2-offload.js +2 -2
- package/dist/host/v2-permissions.d.ts +45 -44
- package/dist/host/v2-permissions.d.ts.map +1 -1
- package/dist/host/v2-permissions.js +48 -49
- package/dist/host/v2-permissions.js.map +1 -1
- package/dist/host/v2-probe-chain.d.ts +93 -0
- package/dist/host/v2-probe-chain.d.ts.map +1 -0
- package/dist/host/v2-probe-chain.js +146 -0
- package/dist/host/v2-probe-chain.js.map +1 -0
- package/dist/host/v2-prune.d.ts +157 -0
- package/dist/host/v2-prune.d.ts.map +1 -0
- package/dist/host/v2-prune.js +507 -0
- package/dist/host/v2-prune.js.map +1 -0
- package/dist/host/v2-retry.d.ts +143 -0
- package/dist/host/v2-retry.d.ts.map +1 -0
- package/dist/host/v2-retry.js +315 -0
- package/dist/host/v2-retry.js.map +1 -0
- package/dist/host/v2-scope.d.ts.map +1 -1
- package/dist/host/v2-scope.js +14 -7
- package/dist/host/v2-scope.js.map +1 -1
- package/dist/host/v2-session-client.d.ts +24 -0
- package/dist/host/v2-session-client.d.ts.map +1 -1
- package/dist/host/v2-session-client.js +243 -3
- package/dist/host/v2-session-client.js.map +1 -1
- package/dist/host/v2-session.d.ts +72 -1
- package/dist/host/v2-session.d.ts.map +1 -1
- package/dist/host/v2-session.js +154 -10
- package/dist/host/v2-session.js.map +1 -1
- package/dist/host/v2-tool.d.ts +2 -3
- package/dist/host/v2-tool.d.ts.map +1 -1
- package/dist/host/v2-tool.js +2 -3
- package/dist/host/v2-tool.js.map +1 -1
- package/dist/host/v2-types.d.ts +25 -0
- package/dist/host/v2-types.d.ts.map +1 -1
- package/dist/host/v2.d.ts +14 -12
- package/dist/host/v2.d.ts.map +1 -1
- package/dist/host/v2.js +500 -73
- package/dist/host/v2.js.map +1 -1
- package/dist/identity.d.ts +27 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +34 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +9 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -27
- package/dist/index.js.map +1 -1
- package/dist/prompts/lead.d.ts +1 -1
- package/dist/prompts/lead.d.ts.map +1 -1
- package/dist/prompts/lead.js +38 -25
- package/dist/prompts/lead.js.map +1 -1
- package/dist/prompts/shared.d.ts +2 -2
- package/dist/prompts/shared.d.ts.map +1 -1
- package/dist/prompts/shared.js +66 -56
- package/dist/prompts/shared.js.map +1 -1
- package/dist/prompts/specialists.d.ts +2 -2
- package/dist/prompts/specialists.d.ts.map +1 -1
- package/dist/prompts/specialists.js +53 -83
- package/dist/prompts/specialists.js.map +1 -1
- package/dist/tm/args-schema.d.ts +7 -32
- package/dist/tm/args-schema.d.ts.map +1 -1
- package/dist/tm/args-schema.js +12 -153
- package/dist/tm/args-schema.js.map +1 -1
- package/dist/tm/board.d.ts +10 -9
- package/dist/tm/board.d.ts.map +1 -1
- package/dist/tm/board.js +11 -10
- package/dist/tm/board.js.map +1 -1
- package/dist/tm/config-files.d.ts +81 -0
- package/dist/tm/config-files.d.ts.map +1 -0
- package/dist/tm/config-files.js +209 -0
- package/dist/tm/config-files.js.map +1 -0
- package/dist/tm/config-layers.d.ts +117 -0
- package/dist/tm/config-layers.d.ts.map +1 -0
- package/dist/tm/config-layers.js +320 -0
- package/dist/tm/config-layers.js.map +1 -0
- package/dist/tm/config.d.ts +10 -101
- package/dist/tm/config.d.ts.map +1 -1
- package/dist/tm/config.js +17 -71
- package/dist/tm/config.js.map +1 -1
- package/dist/tm/dispatch.d.ts +115 -21
- package/dist/tm/dispatch.d.ts.map +1 -1
- package/dist/tm/dispatch.js +424 -67
- package/dist/tm/dispatch.js.map +1 -1
- package/dist/tm/egress.d.ts +19 -7
- package/dist/tm/egress.d.ts.map +1 -1
- package/dist/tm/egress.js +21 -9
- package/dist/tm/egress.js.map +1 -1
- package/dist/tm/index.d.ts +23 -20
- package/dist/tm/index.d.ts.map +1 -1
- package/dist/tm/index.js +117 -116
- package/dist/tm/index.js.map +1 -1
- package/dist/tm/ledger.d.ts +9 -2
- package/dist/tm/ledger.d.ts.map +1 -1
- package/dist/tm/ledger.js +4 -1
- package/dist/tm/ledger.js.map +1 -1
- package/dist/tm/perm-ask.d.ts +3 -3
- package/dist/tm/perm-ask.d.ts.map +1 -1
- package/dist/tm/perm-ask.js +3 -3
- package/dist/tm/perm-ask.js.map +1 -1
- package/dist/tm/pipelines.d.ts +12 -11
- package/dist/tm/pipelines.d.ts.map +1 -1
- package/dist/tm/pipelines.js +40 -207
- package/dist/tm/pipelines.js.map +1 -1
- package/dist/tm/preview.d.ts +1 -1
- package/dist/tm/preview.js +3 -3
- package/dist/tm/preview.js.map +1 -1
- package/dist/tm/refs.d.ts +31 -4
- package/dist/tm/refs.d.ts.map +1 -1
- package/dist/tm/refs.js +48 -4
- package/dist/tm/refs.js.map +1 -1
- package/dist/tm/result.d.ts +1 -1
- package/dist/tm/result.d.ts.map +1 -1
- package/dist/tm/result.js +4 -3
- package/dist/tm/result.js.map +1 -1
- package/dist/tm/search.d.ts +2 -2
- package/dist/tm/search.js +7 -7
- package/dist/tm/search.js.map +1 -1
- package/dist/tm/serp-loop.d.ts +19 -7
- package/dist/tm/serp-loop.d.ts.map +1 -1
- package/dist/tm/serp-loop.js +24 -9
- package/dist/tm/serp-loop.js.map +1 -1
- package/dist/tm/stats.d.ts +12 -27
- package/dist/tm/stats.d.ts.map +1 -1
- package/dist/tm/stats.js +43 -45
- package/dist/tm/stats.js.map +1 -1
- package/dist/tm/store.d.ts +18 -5
- package/dist/tm/store.d.ts.map +1 -1
- package/dist/tm/store.js +39 -9
- package/dist/tm/store.js.map +1 -1
- package/dist/tm/tools.d.ts +7 -5
- package/dist/tm/tools.d.ts.map +1 -1
- package/dist/tm/tools.js +10 -41
- package/dist/tm/tools.js.map +1 -1
- package/dist/tm/webfetch.d.ts +1 -1
- package/dist/tm/webfetch.d.ts.map +1 -1
- package/dist/tm/webfetch.js +38 -23
- package/dist/tm/webfetch.js.map +1 -1
- package/dist/types.d.ts +40 -123
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -21
- package/dist/types.js.map +1 -1
- package/docs/installation-v2.md +109 -33
- package/docs/installation.md +9 -0
- package/docs/research/browser-pane.md +5 -0
- package/docs/v2-port-notes.md +87 -0
- package/package.json +1 -1
- package/scripts/gen-v2-config.mjs +246 -94
- package/scripts/lib/fake-ctx.mjs +75 -0
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
* Refuses to touch a file it did not generate unless --force is given.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"
|
|
32
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync, realpathSync, statSync } from "node:fs"
|
|
33
33
|
import { join } from "node:path"
|
|
34
34
|
import { homedir } from "node:os"
|
|
35
|
+
import { pathToFileURL } from "node:url"
|
|
35
36
|
|
|
36
37
|
const MARKER = "generated by @te-river/opencode-team-mode"
|
|
37
38
|
|
|
@@ -51,6 +52,11 @@ const { commands } = await import(new URL("../dist/commands.js", import.meta.url
|
|
|
51
52
|
const { triplesFromAgentPermission, V1_ONLY_TOOLS } = await import(
|
|
52
53
|
new URL("../dist/host/v2-permissions.js", import.meta.url).href
|
|
53
54
|
)
|
|
55
|
+
// Deletion goes through the repo's own safe remover: win32 `fs.rmSync` silently
|
|
56
|
+
// no-ops on non-ASCII paths, and a reclaim that silently failed would leave the
|
|
57
|
+
// duplicate role it exists to remove. Same rule as every other destructive call
|
|
58
|
+
// site in the plugin (src/fs-safe.ts).
|
|
59
|
+
const { rmForceSafe } = await import(new URL("../dist/fs-safe.js", import.meta.url).href)
|
|
54
60
|
|
|
55
61
|
/** YAML is a superset of JSON, so a JSON scalar is a valid YAML scalar —
|
|
56
62
|
* and the descriptions are full of em-dashes and colons that a hand-rolled
|
|
@@ -107,15 +113,15 @@ const body = (prompt) => {
|
|
|
107
113
|
* and look like success. `test-v2-adapter` group 8 pins both halves.
|
|
108
114
|
*/
|
|
109
115
|
const V2_TEXT = [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
/*
|
|
117
|
+
* The efficiency / file-ladder / pty sentences are NOT here any more: the
|
|
118
|
+
* shared source prompts now say the v2 thing natively (`execute` for batch
|
|
119
|
+
* recon, the native read / grep / glob / shell for retrieval, `shell
|
|
120
|
+
* {background:true}` for slow steps), because this package ships one
|
|
121
|
+
* personality and a translation table is the wrong place to hold the only
|
|
122
|
+
* correct wording. What is left below is what genuinely still differs
|
|
123
|
+
* between what the source says and what this host offers.
|
|
124
|
+
*/
|
|
119
125
|
/*
|
|
120
126
|
* The delegation half of the lead prompt. On v2 the host's tool is named
|
|
121
127
|
* `subagent` — measured, not inferred: a live dispatch reached
|
|
@@ -125,7 +131,9 @@ const V2_TEXT = [
|
|
|
125
131
|
* `background` needs no operator flag — the plugin sets it on every dispatch —
|
|
126
132
|
* so the "pick the shape by rule" instruction is not a choice the model has.
|
|
127
133
|
*/
|
|
128
|
-
|
|
134
|
+
/* The section heading says `subagent` in the SOURCE now (the v1 personality is
|
|
135
|
+
* gone, so nothing reads a v1 wording any more); the fork below still carries
|
|
136
|
+
* the sentences whose v1 shape differs in more than the tool name. */
|
|
129
137
|
["delegation goes through the host's `task` — governed, visible, killable.", "delegation goes through the host's `subagent` — governed, visible, killable."],
|
|
130
138
|
["you will keep following up while they run → `task { background: true }`: the", "you will keep following up while they run → `subagent { background: true }`: the"],
|
|
131
139
|
["`task`. Do not choose background when you would only park waiting for it.",
|
|
@@ -137,46 +145,10 @@ const V2_TEXT = [
|
|
|
137
145
|
["a background `task` names its own session id", "a background `subagent` names its own session id"],
|
|
138
146
|
["a synchronous `task` call, so chaining waits", "a synchronous `subagent` call, so chaining waits"],
|
|
139
147
|
["blocking on `task`", "blocking on `subagent`"],
|
|
140
|
-
/*
|
|
141
|
-
* The file ladder (the tm_read / tm_grep / tm_bash retirement). v2 does not
|
|
142
|
-
* register those three, so every sentence that routes a role through them has
|
|
143
|
-
* to name what is actually on the surface instead — and the native tools are
|
|
144
|
-
* NOT ungoverned here: `execute.after` offloads their oversized results, and
|
|
145
|
-
* the address / R6 red lines ride `permission.evaluate` (both landed BEFORE
|
|
146
|
-
* this fork, which is the ordering the retirement requires).
|
|
147
|
-
*
|
|
148
|
-
* Two words are host-vocabulary, not ours: v2 names the shell tool `shell`,
|
|
149
|
-
* and its `background` argument is documented on the native tool (AGENTS.md,
|
|
150
|
-
* "native shell … has background:true"). The pty line is rewritten around
|
|
151
|
-
* that, NOT around anything I have measured in a live v2 session — a v2
|
|
152
|
-
* session has yet to make a background shell call, and #8/#9 are where that
|
|
153
|
-
* gets checked. Until then the sentence says only "returns at once".
|
|
154
|
-
*/
|
|
155
|
-
["tm_grep / tm_read first, a second lookup only for what it genuinely missed.", "grep / read first, a second lookup only for what it genuinely missed."],
|
|
156
|
-
["All file reads / searches / enumeration go through tm_read / tm_grep / tm_bash.\nThe built-in read/grep/glob/list tools are removed from the tool surface —\nretrying them only wastes a turn. Built-in bash exists only where granted\n(team / implementer / reviewer / tester run commands: build / test / git);\narchitect and researcher have no bash at all — one-off read-only commands\ngo through tm_bash or are reported as a gap.",
|
|
157
|
-
"File reads / searches / enumeration go through the built-in read / grep /\nglob tools — on this host they ARE the governed path: an oversized result comes\nback as a short preview plus a handle, and `tm_fetch` pages the rest, so a wide\ncall is cheap here and still the right move. Built-in shell exists only where\ngranted (team / implementer / reviewer / tester run commands: build / test /\ngit); architect and researcher have no shell at all — a one-off read-only\ncommand they cannot run is reported as a gap, not retried."],
|
|
158
|
-
["code search → tm_grep · enumeration and quick\n probes → tm_bash · multi-file", "code search → grep · enumeration and quick\n probes → glob / shell where granted · multi-file"],
|
|
159
|
-
["use the **built-in bash** tool — not tm_bash. tm_bash hard-blocks them with\nno dialog; built-in bash triggers the official confirmation dialog (once /\nalways / reject). Dangerous commands (rm / git push / npm publish / etc.)\nalways trigger the dialog regardless of tool.",
|
|
160
|
-
"there is no second, ungoverned shell to fall back to on this host: the one\n**shell** tool IS the R6 surface, and its classifier only ever makes a rule\nstricter — an env dump or a delete asks the host, which opens its own dialog\n(once / always / reject). Dangerous commands (rm / git push / npm publish /\netc.) ask regardless of what the config said."],
|
|
161
|
-
["probes toward one goal — tm_read / tm_grep / tm_bash\nOR built-in bash alike — your FIRST move is ONE", "probes toward one goal — read / grep / glob / shell alike — your FIRST move is ONE"],
|
|
162
|
-
["Plain shell probes the governed channel cannot run\n(e.g. env-path checks, which the tm_* channel hard-blocks by\ndesign) go as ONE compound built-in bash command (`a; b; c` in a\nsingle call)",
|
|
163
|
-
"Several cheap probes of one kind go as ONE compound `shell`\ncommand (`a; b; c` in a\nsingle call)"],
|
|
164
|
-
["searches are PTC work;", "searches are one-program work;"],
|
|
165
|
-
["(tm.read / tm.grep / tm.bash ride inside; ALWAYS `return` the", "(the built-in read / grep / shell tools are callable inside it; ALWAYS `return` the"],
|
|
166
|
-
["(or one PTC program), never in two.", "(or one `execute` program), never in two."],
|
|
167
|
-
["run one cheap tm_grep. Do NOT pull big payloads", "run one cheap grep. Do NOT pull big payloads"],
|
|
168
|
-
["or tm_grep + tm_bash yourself)", "or grep + shell yourself)"],
|
|
169
|
-
["into its OWN tm_pty session (returns at once, the user sees the terminal,\n every start passes the official dialog) rather than being chained with\n `;` behind one long bash call. tm_pty returns no transcript — tee it to\n a log and read that log when it reports exited.",
|
|
170
|
-
"into its OWN separate `shell` call rather than being chained with\n `;` behind one long shell call — and when the step is genuinely slow, use\n `shell {background:true}`: the ack gives a shell ID plus the file its output\n streams to, and the host notifies on exit (do NOT poll for it). Have it write\n its own log file and read that log when it reports exited — do not re-run the\n command to see its output."],
|
|
171
|
-
["probes via bash where granted) BEFORE answering from memory.", "probes via shell where granted) BEFORE answering from memory."],
|
|
172
148
|
["FIRST there, because that is the only path with the domain allowlist, the\nper-request dialog and the R6 red lines; an MCP fetcher of the same page\nsilently skips all three (and dumps raw HTML into your context). Fall to a",
|
|
173
149
|
"FIRST there, because that is the only path with the address red line (no\nmetadata / private-range fetch) and the threshold offload; an MCP fetcher of the\nsame page silently skips both (and dumps raw HTML into your context). Fall to a"],
|
|
174
|
-
["- Files/docs → tm_read · code search →", "- Files/docs → read · code search →"],
|
|
175
|
-
[" --help) → built-in bash where granted ·", " --help) → `shell` where granted ·"],
|
|
176
|
-
["## PTC batch orchestration", "## Batch orchestration (Code Mode first)"],
|
|
177
150
|
["file tool at all (architect and researcher have no write/edit/bash; reviewer\n has only the read-only bash, which refuses redirection). Pass the `session`",
|
|
178
151
|
"file tool at all (architect and researcher have no write / edit / shell, and\n reviewer's shell is the host's own, classified per command). Pass the `session`"],
|
|
179
|
-
["- The host stops a bash command after 120 s unless you pass a larger", "- The host stops a shell command after 120 s unless you pass a larger"],
|
|
180
152
|
/*
|
|
181
153
|
* The LEDGER. v2 gives a plugin no `todowrite` (V2_ONLY_ACTIONS names it as a
|
|
182
154
|
* key with no v2 counterpart), so the mandate's tool is the one this plugin
|
|
@@ -202,33 +174,12 @@ const V2_TEXT = [
|
|
|
202
174
|
["Do NOT store task state or oversized content there — todo list and\nboard files own those.",
|
|
203
175
|
"Do NOT store task state or oversized content there — the lead's `tm_ledger`\nand board files own those."],
|
|
204
176
|
["dropped: the list is the user's audit surface (pending / in_progress /\n completed / blocked)", "dropped: the list is the user's audit surface (open / doing /\n done / blocked — `tm_ledger` keeps the states, and `blocked` carries the note\n naming what blocked it)"],
|
|
205
|
-
/*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* `<shell id=… state=… command=…>` message. `shell` and `subagent` are the ONLY two
|
|
212
|
-
* built-ins with a background parameter, which is also why tm_pty stays retired: the pty
|
|
213
|
-
* REST surface is still absent, and the native path has no per-command dialog (and its
|
|
214
|
-
* completion envelope re-enters context carrying the command text — an R6 touchpoint). */
|
|
215
|
-
["either: give each its own call, or run it through tm_pty (non-blocking,\n where granted) and check `status` later. A tm_pty session writes no\n transcript back to you, so tee its output to a file (`<cmd> 2>&1 | tee\n <log>`) and read that file for EVIDENCE once it reports exited.",
|
|
216
|
-
"either: give each its own call, or run it through `shell` with\n `background:true` (it returns at once with a shell ID and the file its output\n streams to, and the host notifies you when it exits — so do NOT poll, and never\n re-run the command to watch it pass again). Either way tee the output\n (`<cmd> 2>&1 | tee\n <log>`) and read that log for EVIDENCE."],
|
|
217
|
-
/* Option A (user decision 2026-09-25): interactive browsing goes to the host's own
|
|
218
|
-
* browser_* tools, because that is the only browser the desktop renders in its side
|
|
219
|
-
* panel (docs/research/browser-pane.md). Governance is NOT taken off the table: the gate
|
|
220
|
-
* lives in src/host/v2-browser-gate.ts (execute.before + a leak-detected fallback that
|
|
221
|
-
* replaces the page with the refusal), and snapshots stay addressable through
|
|
222
|
-
* capKeepingAddressing. tm_browser stays named as the fallback for a host with no
|
|
223
|
-
* desktop browser (CLI / standalone server). Every key below must match the v1 prompt
|
|
224
|
-
* byte-exactly: assertForkApplied() throws when one stops matching. */
|
|
225
|
-
[" URLs → tm_webfetch, JS-rendered pages → tm_browser (network roles only).", " URLs → tm_webfetch, JS-rendered pages → the host's own browser_* tools (they drive\n the side panel the user can actually see), falling back to tm_browser when no desktop browser is connected (network roles only)."],
|
|
226
|
-
["- a visual state (a rendered UI, a chart) → tm_browser", "- a visual state (a rendered UI, a chart) → the host's browser_* tools (browser_tabs_open → browser_snapshot → browser_screenshot), or tm_browser when they are not offered"],
|
|
227
|
-
["## UI verification (tm_browser — you carry it)", "## UI verification (the host's browser_* tools — you carry them)"],
|
|
228
|
-
["For user-visible frontend changes, verify through the governed tm_browser", "For user-visible frontend changes, verify through the host's own browser_* tools (tm_browser only when they are absent)"],
|
|
229
|
-
["If tm_browser is unavailable on this host, the action you need is not", "If neither browser_* nor tm_browser is available on this host, the action you need is not"],
|
|
230
|
-
[" via your governed tm_search / tm_browser / tm_webfetch first,", " via your governed tm_search / tm_webfetch and the host's browser_* tools (or tm_browser when no desktop browser is connected) first,"],
|
|
231
|
-
[" lookups: governed tm_search / tm_browser / tm_webfetch first, then user MCP tools.", " lookups: governed tm_search / tm_webfetch + the host's browser_* tools first, then user MCP tools."],
|
|
177
|
+
/* The browser fork entries are GONE (user decision 2026-10-06): the self-built
|
|
178
|
+
* tm_browser was removed outright, so the shared source prompts now name the
|
|
179
|
+
* host's native browser_* tools natively and there is nothing left to fork.
|
|
180
|
+
* Governance is unchanged: the gate lives in src/host/v2-browser-gate.ts
|
|
181
|
+
* (execute.before + a leak-detected fallback that replaces the page with the
|
|
182
|
+
* refusal), and snapshots stay addressable through capKeepingAddressing. */
|
|
232
183
|
]
|
|
233
184
|
|
|
234
185
|
function forkBody(text, hits) {
|
|
@@ -278,27 +229,228 @@ function emit(dir, name, content) {
|
|
|
278
229
|
written.push(file)
|
|
279
230
|
}
|
|
280
231
|
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
232
|
+
const reclaimed = []
|
|
233
|
+
const keptForeign = []
|
|
234
|
+
const caseNotes = []
|
|
235
|
+
const unverified = []
|
|
236
|
+
|
|
237
|
+
/** Windows is case-insensitive: `team.md` and `Team.md` are ONE file, and
|
|
238
|
+
* `realpathSync` echoes the case it was GIVEN rather than the on-disk case, so
|
|
239
|
+
* a real-path comparison must fold case on win32 — otherwise the reclaim would
|
|
240
|
+
* delete the very file it just wrote. */
|
|
241
|
+
const normPath = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The identity of a file as the OS reports it, or null when `statSync` fails.
|
|
245
|
+
* `dev` + `ino` together name a file on POSIX and on macOS (APFS), where the
|
|
246
|
+
* inode is stable across case spellings of the same name. Windows is the
|
|
247
|
+
* exception: some Node/FS combinations report `ino: 0` for every file, so a
|
|
248
|
+
* zero inode is treated as "unavailable" rather than as a real identity.
|
|
249
|
+
*/
|
|
250
|
+
export function statIdentity(p) {
|
|
251
|
+
try {
|
|
252
|
+
const st = statSync(p)
|
|
253
|
+
return { dev: Number(st.dev), ino: Number(st.ino) }
|
|
254
|
+
} catch {
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
290
257
|
}
|
|
291
|
-
assertForkApplied(forkHits)
|
|
292
258
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Pure file-identity verdict from two stat identities (or null when a stat
|
|
261
|
+
* failed). Returns:
|
|
262
|
+
* "same" — dev+ino both match: the two paths ARE one file
|
|
263
|
+
* "different" — dev+ino both usable and differ: two distinct files
|
|
264
|
+
* "fallback" — inode unavailable (Windows reports 0): the caller may fall
|
|
265
|
+
* back to a case-folded real-path comparison
|
|
266
|
+
* "unknown" — inode unavailable on a non-Windows platform: we CANNOT tell,
|
|
267
|
+
* so the caller must keep the file (fail-safe)
|
|
268
|
+
*
|
|
269
|
+
* The macOS trap this exists for: APFS is case-INSENSITIVE but case-PRESERVING,
|
|
270
|
+
* so `Team.md` and `team.md` are one file while `realpathSync` may echo the
|
|
271
|
+
* spelling it was given. A real-path comparison would then call them different
|
|
272
|
+
* and delete the file that was just written. dev+ino is the platform-neutral
|
|
273
|
+
* answer; when it is unavailable we refuse to delete rather than guess (#48).
|
|
274
|
+
*/
|
|
275
|
+
export function identityVerdict(a, b, platform) {
|
|
276
|
+
const usable = (x) => x != null && Number.isFinite(x.ino) && x.ino !== 0
|
|
277
|
+
if (usable(a) && usable(b)) {
|
|
278
|
+
return a.dev === b.dev && a.ino === b.ino ? "same" : "different"
|
|
279
|
+
}
|
|
280
|
+
return platform === "win32" ? "fallback" : "unknown"
|
|
296
281
|
}
|
|
297
282
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
283
|
+
/**
|
|
284
|
+
* After a deletion, every file in the keep set must still be on disk. A
|
|
285
|
+
* vanished target means the reclaim deleted the file it just wrote — the
|
|
286
|
+
* install is broken and must NOT look like success, so the caller prints the
|
|
287
|
+
* error and exits non-zero. Pure over the `exists` predicate so it is testable
|
|
288
|
+
* without touching the filesystem.
|
|
289
|
+
*/
|
|
290
|
+
export function verifyKeepIntact(keepPaths, exists = existsSync) {
|
|
291
|
+
const missing = keepPaths.filter((p) => !exists(p))
|
|
292
|
+
return { ok: missing.length === 0, missing }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The #48 fail-safe's reporting half: a target that vanished during the reclaim
|
|
297
|
+
* means the install is broken, so it must be LOUD and must not exit 0. Exported
|
|
298
|
+
* so the test can drive the exact sentence and the exit code without having to
|
|
299
|
+
* manufacture a filesystem that defeats the identity guard.
|
|
300
|
+
*/
|
|
301
|
+
export function reportKeepLoss(missing) {
|
|
302
|
+
console.error(`错误:回收后目标文件消失,安装已中止(#48 fail-safe):${missing.join(", ")}`)
|
|
303
|
+
process.exitCode = 1
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Reclaim role/command files this generator wrote in a PREVIOUS run but no
|
|
308
|
+
* longer emits — the `team.md` → `Team.md` rename (#47) is the case that
|
|
309
|
+
* motivated it. On a case-sensitive filesystem (Linux) the old lowercase file
|
|
310
|
+
* survives beside the new one and the host loads TWO Team roles; on a
|
|
311
|
+
* case-insensitive filesystem (Windows, and macOS/APFS by default) the two names
|
|
312
|
+
* are one file, so there is nothing to reclaim and deleting would delete the
|
|
313
|
+
* target.
|
|
314
|
+
*
|
|
315
|
+
* Strict rules:
|
|
316
|
+
* - only a file carrying MARKER is ours to remove; a hand-written file with a
|
|
317
|
+
* stale name is left alone and REPORTED, never silently kept.
|
|
318
|
+
* - "is this the file we just wrote?" is answered by dev+ino first (reliable on
|
|
319
|
+
* POSIX and macOS), then by a case-folded real path on win32 only. When
|
|
320
|
+
* neither can answer, the file is KEPT and reported — a duplicate role is a
|
|
321
|
+
* far smaller failure than a deleted target (#48).
|
|
322
|
+
* - after every deletion the keep set is re-checked; a vanished target prints
|
|
323
|
+
* an error and sets a non-zero exit code.
|
|
324
|
+
* - deletion goes through rmForceSafe (dist/fs-safe.js): win32 fs.rmSync
|
|
325
|
+
* silently no-ops on non-ASCII paths, and a reclaim that silently failed
|
|
326
|
+
* would leave the duplicate role it exists to remove.
|
|
327
|
+
*/
|
|
328
|
+
function reclaimStale(dir, keepNames, label) {
|
|
329
|
+
if (!existsSync(dir)) return
|
|
330
|
+
const keep = new Set(keepNames.map((n) => `${n}.md`))
|
|
331
|
+
const keepPaths = []
|
|
332
|
+
const keepId = new Map()
|
|
333
|
+
for (const n of keep) {
|
|
334
|
+
const p = join(dir, n)
|
|
335
|
+
if (!existsSync(p)) continue
|
|
336
|
+
keepPaths.push(p)
|
|
337
|
+
keepId.set(n, statIdentity(p))
|
|
338
|
+
}
|
|
339
|
+
for (const entry of readdirSync(dir)) {
|
|
340
|
+
if (!entry.endsWith(".md")) continue
|
|
341
|
+
if (keep.has(entry)) continue
|
|
342
|
+
const file = join(dir, entry)
|
|
343
|
+
const fileId = statIdentity(file)
|
|
344
|
+
// Decide "same file as a target?" per keep entry, most reliable test first.
|
|
345
|
+
let sameAs = null
|
|
346
|
+
let undecidable = false
|
|
347
|
+
for (const n of keep) {
|
|
348
|
+
if (!keepId.has(n)) continue
|
|
349
|
+
const verdict = identityVerdict(fileId, keepId.get(n), process.platform)
|
|
350
|
+
if (verdict === "same") {
|
|
351
|
+
sameAs = n
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
if (verdict === "fallback") {
|
|
355
|
+
// win32 with no usable inode: compare case-folded real paths.
|
|
356
|
+
try {
|
|
357
|
+
if (normPath(realpathSync(file)) === normPath(realpathSync(join(dir, n)))) {
|
|
358
|
+
sameAs = n
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
undecidable = true
|
|
363
|
+
}
|
|
364
|
+
} else if (verdict === "unknown") {
|
|
365
|
+
undecidable = true
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (sameAs) {
|
|
369
|
+
// Same file as a target, stored under a different case. There is nothing
|
|
370
|
+
// stale here, and deleting it would delete the file we just wrote.
|
|
371
|
+
caseNotes.push(`保留 ${file} — 与目标 ${sameAs} 是同一个文件(本机不区分大小写),未删除`)
|
|
372
|
+
continue
|
|
373
|
+
}
|
|
374
|
+
if (undecidable) {
|
|
375
|
+
// Neither dev+ino nor a real-path comparison could decide. Fail safe:
|
|
376
|
+
// keep the file and say so — a duplicate role beats a deleted target.
|
|
377
|
+
unverified.push(`保留 ${file} — 无法确认是否与目标 ${[...keep].join(" / ")} 是同一文件,保留不删`)
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
380
|
+
let current
|
|
381
|
+
try {
|
|
382
|
+
current = readFileSync(file, "utf8")
|
|
383
|
+
} catch {
|
|
384
|
+
continue
|
|
385
|
+
}
|
|
386
|
+
if (!current.includes(MARKER)) {
|
|
387
|
+
keptForeign.push(`保留 ${file} — 不是我生成的(无标记),请自行决定`)
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
if (printOnly) {
|
|
391
|
+
reclaimed.push(`回收 ${file} — 本次不再生成该${label}(--print,未落盘)`)
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
rmForceSafe(file)
|
|
395
|
+
if (existsSync(file)) {
|
|
396
|
+
reclaimed.push(`回收失败 ${file} — 仍在磁盘上,请手动删除`)
|
|
397
|
+
} else {
|
|
398
|
+
reclaimed.push(`回收 ${file} — 本次不再生成该${label}(旧文件已清理)`)
|
|
399
|
+
}
|
|
400
|
+
// #48: a deletion must never take a target with it. Re-check the keep set
|
|
401
|
+
// after EVERY real deletion; a vanished target is a broken install.
|
|
402
|
+
const intact = verifyKeepIntact(keepPaths)
|
|
403
|
+
if (!intact.ok) {
|
|
404
|
+
reportKeepLoss(intact.missing)
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function main() {
|
|
411
|
+
const agentsDir = join(baseDir, "agents")
|
|
412
|
+
const forkHits = new Set()
|
|
413
|
+
for (const [id, cfg] of Object.entries(agents)) {
|
|
414
|
+
const { triples } = triplesFromAgentPermission(cfg.permission, { escalateShellAsk: false, agentName: id })
|
|
415
|
+
// An `allow` for a tool v2 never registers would claim a capability that does
|
|
416
|
+
// not exist; V1_ONLY_TOOLS is the same set v2.ts skips when registering.
|
|
417
|
+
const v2Triples = triples.filter((t) => !V1_ONLY_TOOLS.has(t.action))
|
|
418
|
+
const text = forkBody(body(cfg.prompt), forkHits)
|
|
419
|
+
emit(agentsDir, id, agentFrontmatter(cfg, v2Triples) + text + "\n")
|
|
420
|
+
}
|
|
421
|
+
assertForkApplied(forkHits)
|
|
422
|
+
// Reclaim role files a previous run wrote under a name this run no longer emits
|
|
423
|
+
// (the `team.md` → `Team.md` rename, #47). Runs AFTER the writes so the keep
|
|
424
|
+
// set is the current output, and the identity guard keeps it from deleting a
|
|
425
|
+
// file it just wrote on a case-insensitive filesystem.
|
|
426
|
+
reclaimStale(agentsDir, Object.keys(agents), "角色")
|
|
427
|
+
|
|
428
|
+
const commandsDir = join(baseDir, "commands")
|
|
429
|
+
for (const [name, cfg] of Object.entries(commands)) {
|
|
430
|
+
emit(commandsDir, name, commandFrontmatter(cfg) + body(cfg.template) + "\n")
|
|
431
|
+
}
|
|
432
|
+
// Same pass for commands. No command name has changed yet, so this is a no-op
|
|
433
|
+
// today — it is here so a future rename is closed by the same mechanism rather
|
|
434
|
+
// than by a second, hand-written reclaim that could drift from this one.
|
|
435
|
+
reclaimStale(commandsDir, Object.keys(commands), "命令")
|
|
436
|
+
|
|
437
|
+
console.log(`目标:${agentsDir} 与 ${commandsDir}`)
|
|
438
|
+
for (const f of written) console.log(printOnly ? `将写入 ${f}` : `已写入 ${f}`)
|
|
439
|
+
for (const s of skipped) console.log(`跳过 ${s}`)
|
|
440
|
+
for (const r of reclaimed) console.log(r)
|
|
441
|
+
for (const k of keptForeign) console.log(k)
|
|
442
|
+
for (const c of caseNotes) console.log(c)
|
|
443
|
+
for (const u of unverified) console.log(u)
|
|
444
|
+
console.log(
|
|
445
|
+
`\n共 ${written.length} 个文件(角色 ${Object.keys(agents).length} + 命令 ${Object.keys(commands).length})` +
|
|
446
|
+
`${printOnly ? "(--print,未落盘)" : ""}。重启桌面端(或 opencode reload)后 Team 出现在 agent 选择器、/team-* 出现在命令列表。`,
|
|
447
|
+
)
|
|
448
|
+
if (unverified.length) {
|
|
449
|
+
console.log(`注意:${unverified.length} 个文件无法确认是否与目标同一文件,已保留不删(#48 fail-safe)。`)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Run only when invoked as a script. Importing this module (the test suite
|
|
454
|
+
// does, to exercise the pure identity helpers) must NOT write to the user's
|
|
455
|
+
// real config directory — the default `--dir` is `~/.config/opencode`.
|
|
456
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
|
package/scripts/lib/fake-ctx.mjs
CHANGED
|
@@ -20,6 +20,7 @@ export function makeFakeCtx({
|
|
|
20
20
|
agents: seedAgents = [],
|
|
21
21
|
tools: seedTools = [],
|
|
22
22
|
sessionData = null,
|
|
23
|
+
models: seedModels = null,
|
|
23
24
|
} = {}) {
|
|
24
25
|
const registrations = []
|
|
25
26
|
const hooks = new Map()
|
|
@@ -70,6 +71,30 @@ export function makeFakeCtx({
|
|
|
70
71
|
const errorLines = []
|
|
71
72
|
/** #33: which child sessions a stop attempt actually reached, in order. */
|
|
72
73
|
const stopCalls = []
|
|
74
|
+
/** #steer: what each interjection seam actually received, args and all. A test that
|
|
75
|
+
* only read the tool's prose could not tell 插话给了子会话 from 插话给了调用者自己,
|
|
76
|
+
* and that distinction is the whole safety property of the feature. */
|
|
77
|
+
const promptCalls = []
|
|
78
|
+
const syntheticCalls = []
|
|
79
|
+
const inboxListCalls = []
|
|
80
|
+
const inboxCancelCalls = []
|
|
81
|
+
/** #39: the compaction admissions the fake actually received, and how many times the
|
|
82
|
+
* catalog was listed. A test that only read the layer's own counters could not tell
|
|
83
|
+
* "we asked the host to compact" from "we decided to and told nobody". */
|
|
84
|
+
const compactCalls = []
|
|
85
|
+
const modelListCalls = []
|
|
86
|
+
|
|
87
|
+
/** Turn a seeded verdict into a host-shaped method: a function is called with the args
|
|
88
|
+
* (so a test can answer per-session), an Error throws, anything else is returned as-is.
|
|
89
|
+
* Same construction as `stop` above — a fake that always answered `true` would let the
|
|
90
|
+
* tool claim 已受理 against a host that never admitted anything. */
|
|
91
|
+
const verdictMethod = (seed, logArr) => async (args = {}) => {
|
|
92
|
+
logArr.push({ ...(args ?? {}) })
|
|
93
|
+
const v = typeof seed === "function" ? seed(args) : seed
|
|
94
|
+
if (v instanceof Error) throw v
|
|
95
|
+
return v
|
|
96
|
+
}
|
|
97
|
+
const has = (key) => sessionData && Object.prototype.hasOwnProperty.call(sessionData, key)
|
|
73
98
|
|
|
74
99
|
return {
|
|
75
100
|
ctx: {
|
|
@@ -119,9 +144,51 @@ export function makeFakeCtx({
|
|
|
119
144
|
},
|
|
120
145
|
}
|
|
121
146
|
: {}),
|
|
147
|
+
// #steer — the interjection seams, seeded ONLY when a test asks for them, for
|
|
148
|
+
// the same reason `interrupt` is: a host that never admits inbox input must not
|
|
149
|
+
// be able to be described as having accepted one. `prompt` is the documented
|
|
150
|
+
// `{sessionID, text, delivery}` call; `synthetic` is the fallback seam; the
|
|
151
|
+
// inbox ops exist in TWO spellings (nested `session.inbox.list`, which the
|
|
152
|
+
// published operation ids imply, and flat `session["inbox.list"]`), and
|
|
153
|
+
// `inboxFlat` lets a test drive the fallback and pin which one answered.
|
|
154
|
+
...(has("prompt") ? { prompt: verdictMethod(sessionData.prompt, promptCalls) } : {}),
|
|
155
|
+
// #39 — `compact` is seeded ONLY when a test asks for it, for the same reason
|
|
156
|
+
// as `interrupt`: the 2.0.23 probe proved the seam exists (`compact({})` →
|
|
157
|
+
// Missing key at ["sessionID"]), but a fake that always had it would let the
|
|
158
|
+
// layer claim a host that has none.
|
|
159
|
+
...(has("compact") ? { compact: verdictMethod(sessionData.compact, compactCalls) } : {}),
|
|
160
|
+
...(has("synthetic") ? { synthetic: verdictMethod(sessionData.synthetic, syntheticCalls) } : {}),
|
|
161
|
+
...(has("inbox") && sessionData.inbox && !sessionData.inboxFlat
|
|
162
|
+
? {
|
|
163
|
+
inbox: {
|
|
164
|
+
...(sessionData.inbox.list !== undefined
|
|
165
|
+
? { list: verdictMethod(sessionData.inbox.list, inboxListCalls) }
|
|
166
|
+
: {}),
|
|
167
|
+
...(sessionData.inbox.cancel !== undefined
|
|
168
|
+
? { cancel: verdictMethod(sessionData.inbox.cancel, inboxCancelCalls) }
|
|
169
|
+
: {}),
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
: {}),
|
|
173
|
+
...(has("inbox") && sessionData.inbox && sessionData.inboxFlat
|
|
174
|
+
? {
|
|
175
|
+
"inbox.list": verdictMethod(sessionData.inbox.list, inboxListCalls),
|
|
176
|
+
"inbox.cancel": verdictMethod(sessionData.inbox.cancel, inboxCancelCalls),
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
122
179
|
}
|
|
123
180
|
: { hook: mkHook("session") },
|
|
124
181
|
shell: { hook: mkHook("shell") },
|
|
182
|
+
// `model.list()` measured on 2.0.23: `{location, data:[{id, modelID, providerID,
|
|
183
|
+
// …, limit:{context, output}}]}`. The shape below is that shape, because the early
|
|
184
|
+
// compaction layer reads `limit.context` as the denominator and a fake with a
|
|
185
|
+
// friendlier shape would test the reading, not the contract.
|
|
186
|
+
model: {
|
|
187
|
+
async list(opts = {}) {
|
|
188
|
+
modelListCalls.push({ ...(opts ?? {}) })
|
|
189
|
+
return { location: { directory }, data: seedModels ?? [] }
|
|
190
|
+
},
|
|
191
|
+
},
|
|
125
192
|
storage: {
|
|
126
193
|
_map: new Map(),
|
|
127
194
|
async get(k) { return this._map.get(k) },
|
|
@@ -150,6 +217,14 @@ export function makeFakeCtx({
|
|
|
150
217
|
/** #33 — the stop attempts this fake actually received, so a test can pin that a
|
|
151
218
|
* cancel reached the host with the child's id rather than trusting the tool's prose. */
|
|
152
219
|
stopCalls,
|
|
220
|
+
/** #steer — the same for the interjection seams: the args each one really got. */
|
|
221
|
+
promptCalls,
|
|
222
|
+
syntheticCalls,
|
|
223
|
+
inboxListCalls,
|
|
224
|
+
inboxCancelCalls,
|
|
225
|
+
/** #39 — the compaction admissions and the catalog reads, with their args. */
|
|
226
|
+
compactCalls,
|
|
227
|
+
modelListCalls,
|
|
153
228
|
}
|
|
154
229
|
}
|
|
155
230
|
|