@te-river/opencode-team-mode 1.6.1 → 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 (201) 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/note.d.ts.map +1 -1
  28. package/dist/host/note.js +8 -5
  29. package/dist/host/note.js.map +1 -1
  30. package/dist/host/v2-browser-gate.d.ts +19 -1
  31. package/dist/host/v2-browser-gate.d.ts.map +1 -1
  32. package/dist/host/v2-browser-gate.js +47 -6
  33. package/dist/host/v2-browser-gate.js.map +1 -1
  34. package/dist/host/v2-capabilities.d.ts +43 -0
  35. package/dist/host/v2-capabilities.d.ts.map +1 -1
  36. package/dist/host/v2-capabilities.js +77 -1
  37. package/dist/host/v2-capabilities.js.map +1 -1
  38. package/dist/host/v2-compaction.d.ts +129 -0
  39. package/dist/host/v2-compaction.d.ts.map +1 -0
  40. package/dist/host/v2-compaction.js +346 -0
  41. package/dist/host/v2-compaction.js.map +1 -0
  42. package/dist/host/v2-default-agent.d.ts +37 -0
  43. package/dist/host/v2-default-agent.d.ts.map +1 -0
  44. package/dist/host/v2-default-agent.js +128 -0
  45. package/dist/host/v2-default-agent.js.map +1 -0
  46. package/dist/host/v2-events.d.ts +2 -0
  47. package/dist/host/v2-events.d.ts.map +1 -1
  48. package/dist/host/v2-events.js +27 -0
  49. package/dist/host/v2-events.js.map +1 -1
  50. package/dist/host/v2-guard.d.ts +178 -0
  51. package/dist/host/v2-guard.d.ts.map +1 -1
  52. package/dist/host/v2-guard.js +324 -8
  53. package/dist/host/v2-guard.js.map +1 -1
  54. package/dist/host/v2-offload.d.ts +1 -1
  55. package/dist/host/v2-offload.js +2 -2
  56. package/dist/host/v2-permissions.d.ts +45 -44
  57. package/dist/host/v2-permissions.d.ts.map +1 -1
  58. package/dist/host/v2-permissions.js +48 -49
  59. package/dist/host/v2-permissions.js.map +1 -1
  60. package/dist/host/v2-probe-chain.d.ts +93 -0
  61. package/dist/host/v2-probe-chain.d.ts.map +1 -0
  62. package/dist/host/v2-probe-chain.js +146 -0
  63. package/dist/host/v2-probe-chain.js.map +1 -0
  64. package/dist/host/v2-prune.d.ts +157 -0
  65. package/dist/host/v2-prune.d.ts.map +1 -0
  66. package/dist/host/v2-prune.js +507 -0
  67. package/dist/host/v2-prune.js.map +1 -0
  68. package/dist/host/v2-retry.d.ts +143 -0
  69. package/dist/host/v2-retry.d.ts.map +1 -0
  70. package/dist/host/v2-retry.js +315 -0
  71. package/dist/host/v2-retry.js.map +1 -0
  72. package/dist/host/v2-scope.d.ts.map +1 -1
  73. package/dist/host/v2-scope.js +14 -7
  74. package/dist/host/v2-scope.js.map +1 -1
  75. package/dist/host/v2-session-client.d.ts +62 -7
  76. package/dist/host/v2-session-client.d.ts.map +1 -1
  77. package/dist/host/v2-session-client.js +485 -26
  78. package/dist/host/v2-session-client.js.map +1 -1
  79. package/dist/host/v2-session.d.ts +72 -1
  80. package/dist/host/v2-session.d.ts.map +1 -1
  81. package/dist/host/v2-session.js +154 -10
  82. package/dist/host/v2-session.js.map +1 -1
  83. package/dist/host/v2-tool.d.ts +2 -3
  84. package/dist/host/v2-tool.d.ts.map +1 -1
  85. package/dist/host/v2-tool.js +2 -3
  86. package/dist/host/v2-tool.js.map +1 -1
  87. package/dist/host/v2-types.d.ts +25 -0
  88. package/dist/host/v2-types.d.ts.map +1 -1
  89. package/dist/host/v2.d.ts +14 -12
  90. package/dist/host/v2.d.ts.map +1 -1
  91. package/dist/host/v2.js +648 -74
  92. package/dist/host/v2.js.map +1 -1
  93. package/dist/identity.d.ts +27 -0
  94. package/dist/identity.d.ts.map +1 -0
  95. package/dist/identity.js +34 -0
  96. package/dist/identity.js.map +1 -0
  97. package/dist/index.d.ts +9 -24
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +17 -27
  100. package/dist/index.js.map +1 -1
  101. package/dist/prompts/lead.d.ts +1 -1
  102. package/dist/prompts/lead.d.ts.map +1 -1
  103. package/dist/prompts/lead.js +44 -27
  104. package/dist/prompts/lead.js.map +1 -1
  105. package/dist/prompts/shared.d.ts +2 -2
  106. package/dist/prompts/shared.d.ts.map +1 -1
  107. package/dist/prompts/shared.js +74 -57
  108. package/dist/prompts/shared.js.map +1 -1
  109. package/dist/prompts/specialists.d.ts +2 -2
  110. package/dist/prompts/specialists.d.ts.map +1 -1
  111. package/dist/prompts/specialists.js +53 -83
  112. package/dist/prompts/specialists.js.map +1 -1
  113. package/dist/tm/args-schema.d.ts +7 -32
  114. package/dist/tm/args-schema.d.ts.map +1 -1
  115. package/dist/tm/args-schema.js +12 -153
  116. package/dist/tm/args-schema.js.map +1 -1
  117. package/dist/tm/board.d.ts +10 -9
  118. package/dist/tm/board.d.ts.map +1 -1
  119. package/dist/tm/board.js +11 -10
  120. package/dist/tm/board.js.map +1 -1
  121. package/dist/tm/config-files.d.ts +81 -0
  122. package/dist/tm/config-files.d.ts.map +1 -0
  123. package/dist/tm/config-files.js +209 -0
  124. package/dist/tm/config-files.js.map +1 -0
  125. package/dist/tm/config-layers.d.ts +117 -0
  126. package/dist/tm/config-layers.d.ts.map +1 -0
  127. package/dist/tm/config-layers.js +320 -0
  128. package/dist/tm/config-layers.js.map +1 -0
  129. package/dist/tm/config.d.ts +10 -101
  130. package/dist/tm/config.d.ts.map +1 -1
  131. package/dist/tm/config.js +17 -71
  132. package/dist/tm/config.js.map +1 -1
  133. package/dist/tm/dispatch.d.ts +154 -22
  134. package/dist/tm/dispatch.d.ts.map +1 -1
  135. package/dist/tm/dispatch.js +559 -80
  136. package/dist/tm/dispatch.js.map +1 -1
  137. package/dist/tm/egress.d.ts +19 -7
  138. package/dist/tm/egress.d.ts.map +1 -1
  139. package/dist/tm/egress.js +21 -9
  140. package/dist/tm/egress.js.map +1 -1
  141. package/dist/tm/index.d.ts +23 -20
  142. package/dist/tm/index.d.ts.map +1 -1
  143. package/dist/tm/index.js +117 -116
  144. package/dist/tm/index.js.map +1 -1
  145. package/dist/tm/ledger.d.ts +9 -2
  146. package/dist/tm/ledger.d.ts.map +1 -1
  147. package/dist/tm/ledger.js +4 -1
  148. package/dist/tm/ledger.js.map +1 -1
  149. package/dist/tm/perm-ask.d.ts +3 -3
  150. package/dist/tm/perm-ask.d.ts.map +1 -1
  151. package/dist/tm/perm-ask.js +3 -3
  152. package/dist/tm/perm-ask.js.map +1 -1
  153. package/dist/tm/pipelines.d.ts +12 -11
  154. package/dist/tm/pipelines.d.ts.map +1 -1
  155. package/dist/tm/pipelines.js +40 -207
  156. package/dist/tm/pipelines.js.map +1 -1
  157. package/dist/tm/preview.d.ts +1 -1
  158. package/dist/tm/preview.js +3 -3
  159. package/dist/tm/preview.js.map +1 -1
  160. package/dist/tm/refs.d.ts +31 -4
  161. package/dist/tm/refs.d.ts.map +1 -1
  162. package/dist/tm/refs.js +48 -4
  163. package/dist/tm/refs.js.map +1 -1
  164. package/dist/tm/result.d.ts +1 -1
  165. package/dist/tm/result.d.ts.map +1 -1
  166. package/dist/tm/result.js +4 -3
  167. package/dist/tm/result.js.map +1 -1
  168. package/dist/tm/search.d.ts +2 -2
  169. package/dist/tm/search.js +7 -7
  170. package/dist/tm/search.js.map +1 -1
  171. package/dist/tm/serp-loop.d.ts +19 -7
  172. package/dist/tm/serp-loop.d.ts.map +1 -1
  173. package/dist/tm/serp-loop.js +24 -9
  174. package/dist/tm/serp-loop.js.map +1 -1
  175. package/dist/tm/stats.d.ts +31 -27
  176. package/dist/tm/stats.d.ts.map +1 -1
  177. package/dist/tm/stats.js +170 -58
  178. package/dist/tm/stats.js.map +1 -1
  179. package/dist/tm/store.d.ts +18 -5
  180. package/dist/tm/store.d.ts.map +1 -1
  181. package/dist/tm/store.js +39 -9
  182. package/dist/tm/store.js.map +1 -1
  183. package/dist/tm/tools.d.ts +7 -5
  184. package/dist/tm/tools.d.ts.map +1 -1
  185. package/dist/tm/tools.js +10 -41
  186. package/dist/tm/tools.js.map +1 -1
  187. package/dist/tm/webfetch.d.ts +1 -1
  188. package/dist/tm/webfetch.d.ts.map +1 -1
  189. package/dist/tm/webfetch.js +38 -23
  190. package/dist/tm/webfetch.js.map +1 -1
  191. package/dist/types.d.ts +40 -123
  192. package/dist/types.d.ts.map +1 -1
  193. package/dist/types.js +7 -21
  194. package/dist/types.js.map +1 -1
  195. package/docs/installation-v2.md +124 -39
  196. package/docs/installation.md +9 -0
  197. package/docs/research/browser-pane.md +5 -0
  198. package/docs/v2-port-notes.md +500 -315
  199. package/package.json +1 -1
  200. package/scripts/gen-v2-config.mjs +246 -94
  201. package/scripts/lib/fake-ctx.mjs +244 -146
@@ -1,146 +1,244 @@
1
- // A fake OpenCode v2 plugin context for the adapter tests.
2
- //
3
- // There was no such thing before the v2 port: every existing suite builds a v1
4
- // tool ctx as an inline literal ({directory, sessionID, agent, ask}), because
5
- // v1 hands tools a plain object. v2 hands the PLUGIN a context of domains with
6
- // transform()/hook() registrars, so testing the v2 personality needs a host
7
- // double that records what was registered and can replay a hook.
8
- //
9
- // Deliberately faithful on the three points that matter:
10
- // - transform(cb) runs the callback SYNCHRONOUSLY against a live editor and
11
- // returns a disposable (a fake that defers would let a plugin pass tests
12
- // against an API shape the host does not have);
13
- // - hook callbacks are callable by the test so a hook body can be exercised
14
- // with a real payload;
15
- // - nothing here touches the filesystem or the network.
16
-
17
- export function makeFakeCtx({
18
- directory = process.cwd(),
19
- options = {},
20
- agents: seedAgents = [],
21
- tools: seedTools = [],
22
- sessionData = null,
23
- } = {}) {
24
- const registrations = []
25
- const hooks = new Map()
26
-
27
- const disposable = (label) => {
28
- const rec = { label, disposed: false, dispose: async () => { rec.disposed = true } }
29
- registrations.push(rec)
30
- return rec
31
- }
32
-
33
- const toolMap = new Map()
34
- for (const t of seedTools) toolMap.set(t.name ?? t.id, { ...t, id: t.id ?? t.name })
35
- const toolEditor = {
36
- list: () => [...toolMap.values()],
37
- get: (id) => toolMap.get(id),
38
- add: (tool) => { toolMap.set(tool.name, { ...tool, id: tool.name }) },
39
- update: (id, fn) => { const cur = toolMap.get(id); if (cur) fn(cur) },
40
- remove: (id) => { toolMap.delete(id) },
41
- namespace: () => {},
42
- }
43
-
44
- const agentMap = new Map()
45
- for (const a of seedAgents) agentMap.set(a.id, { ...a })
46
- const agentEditor = {
47
- list: () => [...agentMap.values()],
48
- get: (id) => agentMap.get(id),
49
- default: (id) => { agentEditor.__default = id },
50
- update: (id, fn) => { const cur = agentMap.get(id); if (cur) fn(cur) },
51
- remove: (id) => { agentMap.delete(id) },
52
- }
53
-
54
- const hookHandle = (domain, name) => {
55
- const key = `${domain}.${name}`
56
- const entry = hooks.get(key) ?? { key, calls: [], fire: async (input) => {
57
- for (const cb of entry.handlers) await cb(input)
58
- return input
59
- }, handlers: [] }
60
- hooks.set(key, entry)
61
- return entry
62
- }
63
-
64
- const mkHook = (domain) => async (name, callback) => {
65
- hookHandle(domain, name).handlers.push(callback)
66
- return disposable(`${domain}.hook(${name})`)
67
- }
68
-
69
- const warnLines = []
70
- const errorLines = []
71
-
72
- return {
73
- ctx: {
74
- location: { directory, project: { directory } },
75
- options,
76
- tool: {
77
- transform: async (cb) => { cb(toolEditor); return disposable("tool.transform") },
78
- list: async () => [...toolMap.values()],
79
- hook: mkHook("tool"),
80
- reload: async () => {},
81
- },
82
- agent: {
83
- transform: async (cb) => { cb(agentEditor); return disposable("agent.transform") },
84
- list: async () => [...agentMap.values()],
85
- },
86
- permission: { hook: mkHook("permission"), list: async () => [], get: async () => ({}), reply: async () => ({}) },
87
- // `session.get` / `session.context` exist ONLY when a test seeds them, because a fake
88
- // that always answers would let a plugin pass while the real host refuses — and the
89
- // shapes below are the ones measured on 2.0.18 (`get` → {id,parentID,…}, `context` →
90
- // an array of {id,time,text,type}), not a guess. See src/host/v2-transcript.ts.
91
- session: sessionData
92
- ? {
93
- hook: mkHook("session"),
94
- async get({ sessionID } = {}) {
95
- const s = sessionData.sessions?.[sessionID]
96
- if (!s) throw new Error(`Session not found: ${sessionID}`)
97
- return { id: sessionID, ...s }
98
- },
99
- async context({ sessionID } = {}) {
100
- return sessionData.messages?.[sessionID] ?? []
101
- },
102
- }
103
- : { hook: mkHook("session") },
104
- shell: { hook: mkHook("shell") },
105
- storage: {
106
- _map: new Map(),
107
- async get(k) { return this._map.get(k) },
108
- async set(k, v) { this._map.set(k, v) },
109
- async remove(k) { this._map.delete(k) },
110
- async scan() { return { entries: [] } },
111
- },
112
- event: { subscribe: () => ({ async [Symbol.asyncIterator]() { return { done: true, value: undefined } } }) },
113
- app: { name: "opencode-fake", version: "2.0.16", channel: "test" },
114
- },
115
- /** Introspection for assertions. */
116
- registrations,
117
- tools: toolEditor,
118
- agents: agentEditor,
119
- /** Introspection for assertions. Only the FIRST dot separates the domain
120
- * from the hook name — `tool.execute.before` is domain "tool" + hook
121
- * "execute.before", and splitting on every dot would look up a key that
122
- * nothing ever registered, so a working hook would read as an absent one. */
123
- hook: (name) => {
124
- const [domain, ...rest] = name.split(".")
125
- return hookHandle(domain, rest.join("."))
126
- },
127
- hookNames: () => [...hooks.keys()],
128
- consoleWarn: warnLines,
129
- consoleError: errorLines,
130
- }
131
- }
132
-
133
- /** Capture what the personality logs during setup — a v2 plugin has no toast,
134
- * so the server log IS the user-visible channel for these gaps. */
135
- export function withCapturedConsole(fn) {
136
- const warns = []
137
- const errors = []
138
- const w = console.warn
139
- const e = console.error
140
- console.warn = (...a) => { warns.push(a.join(" ")) }
141
- console.error = (...a) => { errors.push(a.join(" ")) }
142
- return Promise.resolve()
143
- .then(fn)
144
- .finally(() => { console.warn = w; console.error = e })
145
- .then((value) => ({ value, warns, errors }))
146
- }
1
+ // A fake OpenCode v2 plugin context for the adapter tests.
2
+ //
3
+ // There was no such thing before the v2 port: every existing suite builds a v1
4
+ // tool ctx as an inline literal ({directory, sessionID, agent, ask}), because
5
+ // v1 hands tools a plain object. v2 hands the PLUGIN a context of domains with
6
+ // transform()/hook() registrars, so testing the v2 personality needs a host
7
+ // double that records what was registered and can replay a hook.
8
+ //
9
+ // Deliberately faithful on the three points that matter:
10
+ // - transform(cb) runs the callback SYNCHRONOUSLY against a live editor and
11
+ // returns a disposable (a fake that defers would let a plugin pass tests
12
+ // against an API shape the host does not have);
13
+ // - hook callbacks are callable by the test so a hook body can be exercised
14
+ // with a real payload;
15
+ // - nothing here touches the filesystem or the network.
16
+
17
+ export function makeFakeCtx({
18
+ directory = process.cwd(),
19
+ options = {},
20
+ agents: seedAgents = [],
21
+ tools: seedTools = [],
22
+ sessionData = null,
23
+ models: seedModels = null,
24
+ } = {}) {
25
+ const registrations = []
26
+ const hooks = new Map()
27
+
28
+ const disposable = (label) => {
29
+ const rec = { label, disposed: false, dispose: async () => { rec.disposed = true } }
30
+ registrations.push(rec)
31
+ return rec
32
+ }
33
+
34
+ const toolMap = new Map()
35
+ for (const t of seedTools) toolMap.set(t.name ?? t.id, { ...t, id: t.id ?? t.name })
36
+ const toolEditor = {
37
+ list: () => [...toolMap.values()],
38
+ get: (id) => toolMap.get(id),
39
+ add: (tool) => { toolMap.set(tool.name, { ...tool, id: tool.name }) },
40
+ update: (id, fn) => { const cur = toolMap.get(id); if (cur) fn(cur) },
41
+ remove: (id) => { toolMap.delete(id) },
42
+ namespace: () => {},
43
+ }
44
+
45
+ const agentMap = new Map()
46
+ for (const a of seedAgents) agentMap.set(a.id, { ...a })
47
+ const agentEditor = {
48
+ list: () => [...agentMap.values()],
49
+ get: (id) => agentMap.get(id),
50
+ default: (id) => { agentEditor.__default = id },
51
+ update: (id, fn) => { const cur = agentMap.get(id); if (cur) fn(cur) },
52
+ remove: (id) => { agentMap.delete(id) },
53
+ }
54
+
55
+ const hookHandle = (domain, name) => {
56
+ const key = `${domain}.${name}`
57
+ const entry = hooks.get(key) ?? { key, calls: [], fire: async (input) => {
58
+ for (const cb of entry.handlers) await cb(input)
59
+ return input
60
+ }, handlers: [] }
61
+ hooks.set(key, entry)
62
+ return entry
63
+ }
64
+
65
+ const mkHook = (domain) => async (name, callback) => {
66
+ hookHandle(domain, name).handlers.push(callback)
67
+ return disposable(`${domain}.hook(${name})`)
68
+ }
69
+
70
+ const warnLines = []
71
+ const errorLines = []
72
+ /** #33: which child sessions a stop attempt actually reached, in order. */
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)
98
+
99
+ return {
100
+ ctx: {
101
+ location: { directory, project: { directory } },
102
+ options,
103
+ tool: {
104
+ transform: async (cb) => { cb(toolEditor); return disposable("tool.transform") },
105
+ list: async () => [...toolMap.values()],
106
+ hook: mkHook("tool"),
107
+ reload: async () => {},
108
+ },
109
+ agent: {
110
+ transform: async (cb) => { cb(agentEditor); return disposable("agent.transform") },
111
+ list: async () => [...agentMap.values()],
112
+ },
113
+ permission: { hook: mkHook("permission"), list: async () => [], get: async () => ({}), reply: async () => ({}) },
114
+ // `session.get` / `session.context` exist ONLY when a test seeds them, because a fake
115
+ // that always answers would let a plugin pass while the real host refuses — and the
116
+ // shapes below are the ones measured on 2.0.18 (`get` → {id,parentID,…}, `context` →
117
+ // an array of {id,time,text,type}), not a guess. See src/host/v2-transcript.ts.
118
+ //
119
+ // `session.interrupt` (#33) is seeded through `stop`, so a test can hand back each of
120
+ // the host's documented answers separately — `{interrupted:true}` (an active execution
121
+ // was stopped), `{interrupted:false}` (the idle no-op), no boolean at all, or a throw.
122
+ // A fake that always said `true` would let the tool claim 已停止 against a host that
123
+ // never stops anything, which is the exact overstatement the real code refuses.
124
+ session: sessionData
125
+ ? {
126
+ hook: mkHook("session"),
127
+ async get({ sessionID } = {}) {
128
+ const s = sessionData.sessions?.[sessionID]
129
+ if (!s) throw new Error(`Session not found: ${sessionID}`)
130
+ return { id: sessionID, ...s }
131
+ },
132
+ async context({ sessionID } = {}) {
133
+ return sessionData.messages?.[sessionID] ?? []
134
+ },
135
+ ...(sessionData.stop
136
+ ? {
137
+ async interrupt({ sessionID } = {}) {
138
+ stopCalls.push({ sessionID })
139
+ const verdict = typeof sessionData.stop === "function"
140
+ ? sessionData.stop(sessionID)
141
+ : sessionData.stop
142
+ if (verdict instanceof Error) throw verdict
143
+ return verdict
144
+ },
145
+ }
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
+ : {}),
179
+ }
180
+ : { hook: mkHook("session") },
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
+ },
192
+ storage: {
193
+ _map: new Map(),
194
+ async get(k) { return this._map.get(k) },
195
+ async set(k, v) { this._map.set(k, v) },
196
+ async remove(k) { this._map.delete(k) },
197
+ async scan() { return { entries: [] } },
198
+ },
199
+ event: { subscribe: () => ({ async [Symbol.asyncIterator]() { return { done: true, value: undefined } } }) },
200
+ app: { name: "opencode-fake", version: "2.0.16", channel: "test" },
201
+ },
202
+ /** Introspection for assertions. */
203
+ registrations,
204
+ tools: toolEditor,
205
+ agents: agentEditor,
206
+ /** Introspection for assertions. Only the FIRST dot separates the domain
207
+ * from the hook name — `tool.execute.before` is domain "tool" + hook
208
+ * "execute.before", and splitting on every dot would look up a key that
209
+ * nothing ever registered, so a working hook would read as an absent one. */
210
+ hook: (name) => {
211
+ const [domain, ...rest] = name.split(".")
212
+ return hookHandle(domain, rest.join("."))
213
+ },
214
+ hookNames: () => [...hooks.keys()],
215
+ consoleWarn: warnLines,
216
+ consoleError: errorLines,
217
+ /** #33 — the stop attempts this fake actually received, so a test can pin that a
218
+ * cancel reached the host with the child's id rather than trusting the tool's prose. */
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,
228
+ }
229
+ }
230
+
231
+ /** Capture what the personality logs during setup — a v2 plugin has no toast,
232
+ * so the server log IS the user-visible channel for these gaps. */
233
+ export function withCapturedConsole(fn) {
234
+ const warns = []
235
+ const errors = []
236
+ const w = console.warn
237
+ const e = console.error
238
+ console.warn = (...a) => { warns.push(a.join(" ")) }
239
+ console.error = (...a) => { errors.push(a.join(" ")) }
240
+ return Promise.resolve()
241
+ .then(fn)
242
+ .finally(() => { console.warn = w; console.error = e })
243
+ .then((value) => ({ value, warns, errors }))
244
+ }