pantheon-opencode 1.4.2 → 1.5.0-beta.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.
- package/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
- package/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +158 -1189
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +344 -15
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +344 -15
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +6 -2
- package/src/pantheon/delegate-manager.ts +384 -0
- package/src/pantheon/delegate-task-adapter.ts +444 -0
- package/src/pantheon/delegation-notify.ts +35 -20
- package/src/pantheon/model-command.ts +86 -32
- package/src/pantheon/token-opt.ts +555 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +836 -0
- package/src/plugin.ts +57 -10
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +22 -16
- package/src/skills/README.md +0 -2
package/src/plugin-v2.ts
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pantheon V2 Plugin — full orchestration plugin for OpenCode V2.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - 9 orchestration tools via V2 tool.transform (with V1 bridge fallback)
|
|
6
|
+
* - 4 event subscriptions (session.created, idle, error, compacted)
|
|
7
|
+
* - Session hooks (prompt, context) and tool hooks (execute.before/after)
|
|
8
|
+
* - Permission hooks for custom authorization
|
|
9
|
+
*
|
|
10
|
+
* V2 Plugin API surface (confirmed by investigation):
|
|
11
|
+
* ctx.tool.transform / ctx.event.subscribe / ctx.session.hook /
|
|
12
|
+
* ctx.tool.hook / ctx.permission.hook
|
|
13
|
+
*
|
|
14
|
+
* When a V2 API domain is unavailable, the feature is gracefully skipped
|
|
15
|
+
* and documented in V2_UNSUPPORTED_FEATURES. The plugin never breaks.
|
|
16
|
+
*
|
|
17
|
+
* @module plugin-v2
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
import type {
|
|
22
|
+
AgentDraft,
|
|
23
|
+
CatalogDraft,
|
|
24
|
+
CommandDraft,
|
|
25
|
+
PluginContext,
|
|
26
|
+
ReferenceDraft,
|
|
27
|
+
SkillDraft,
|
|
28
|
+
} from '@opencode-ai/plugin/v2/promise'
|
|
29
|
+
import { define } from '@opencode-ai/plugin/v2/promise'
|
|
30
|
+
import {
|
|
31
|
+
getV2BridgeFromContext,
|
|
32
|
+
type PantheonV2Bridge,
|
|
33
|
+
type V2ContextLike,
|
|
34
|
+
} from './pantheon/v2-bridge.ts'
|
|
35
|
+
|
|
36
|
+
// ─── Unsupported Features Registry ───────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Features not yet supported by the installed V2 plugin API.
|
|
40
|
+
* Updated dynamically at setup time based on actual API availability.
|
|
41
|
+
* Initialized with features known to be absent from the V2 promise API;
|
|
42
|
+
* additional features are appended during setup if their API is missing.
|
|
43
|
+
*/
|
|
44
|
+
export const V2_UNSUPPORTED_FEATURES: string[] = ['legacy-hooks']
|
|
45
|
+
|
|
46
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const POLICY_MARKER = '<!-- pantheon-v2-policy -->'
|
|
49
|
+
const POLICY = `${POLICY_MARKER}\nFollow Pantheon routing policy: delegate implementation work to the named specialist and do not claim work was performed without verification.`
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* SystemPart shape sniffing (beta 19192 `Schema validation failed` in
|
|
53
|
+
* system[N]): host sends SystemPart objects ({type,text}) but pinned SDK
|
|
54
|
+
* (@opencode-ai/plugin 1.18.18) only knows string/string[]. Detect at runtime.
|
|
55
|
+
* Never throws (fail-open).
|
|
56
|
+
*/
|
|
57
|
+
function systemEntryText(entry: unknown): string | null {
|
|
58
|
+
try {
|
|
59
|
+
if (typeof entry === 'string') return entry
|
|
60
|
+
if (
|
|
61
|
+
typeof entry === 'object' &&
|
|
62
|
+
entry !== null &&
|
|
63
|
+
typeof (entry as { text?: unknown }).text === 'string'
|
|
64
|
+
) {
|
|
65
|
+
return (entry as { text: string }).text
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasPolicyMarker(arr: unknown[]): boolean {
|
|
74
|
+
try {
|
|
75
|
+
return arr.some((s) => {
|
|
76
|
+
const t = systemEntryText(s)
|
|
77
|
+
return t?.includes(POLICY_MARKER) ?? false
|
|
78
|
+
})
|
|
79
|
+
} catch {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function usesObjectShape(arr: unknown[]): boolean {
|
|
85
|
+
try {
|
|
86
|
+
return arr.some(
|
|
87
|
+
(s) =>
|
|
88
|
+
typeof s === 'object' && s !== null && typeof (s as { text?: unknown }).text === 'string',
|
|
89
|
+
)
|
|
90
|
+
} catch {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toSystemEntry(text: string, useObject: boolean): string | { type: 'text'; text: string } {
|
|
96
|
+
return useObject ? { type: 'text', text } : text
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Add a feature to the unsupported list (idempotent).
|
|
101
|
+
*/
|
|
102
|
+
function markUnsupported(feature: string): void {
|
|
103
|
+
if (!V2_UNSUPPORTED_FEATURES.includes(feature)) {
|
|
104
|
+
V2_UNSUPPORTED_FEATURES.push(feature)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── V1 Bridge Integration ──────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Module-level bridge reference. Set via `setV2Bridge()` from V1 plugin
|
|
112
|
+
* after singletons are initialized. Tools check this first (fast path),
|
|
113
|
+
* then fall back to `ctx.options` retrieval.
|
|
114
|
+
*/
|
|
115
|
+
let v1Bridge: PantheonV2Bridge | null = null
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Set the V1 bridge from the V1 plugin's setup.
|
|
119
|
+
* Called once during V1 plugin initialization — the bridge is then
|
|
120
|
+
* available to all V2 tool handlers and event hooks.
|
|
121
|
+
*/
|
|
122
|
+
export function setV2Bridge(bridge: PantheonV2Bridge): void {
|
|
123
|
+
v1Bridge = bridge
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the V1 bridge, checking module-level first, then ctx.options.
|
|
128
|
+
* Returns null when no bridge is available (V2 standalone mode).
|
|
129
|
+
*/
|
|
130
|
+
function resolveBridge(ctx?: V2ContextLike): PantheonV2Bridge | null {
|
|
131
|
+
if (v1Bridge != null) return v1Bridge
|
|
132
|
+
if (ctx != null) return getV2BridgeFromContext(ctx)
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── V2 Transform Functions ─────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function transformAgents(draft: AgentDraft): void {
|
|
139
|
+
// Beta 19192 guard (issue #92): draft may not have `list`, or list()
|
|
140
|
+
// may return a non-iterable. Never throw — mark unsupported and return.
|
|
141
|
+
let agents: Iterable<{ id: string }>
|
|
142
|
+
try {
|
|
143
|
+
const maybeList = (draft as unknown as { list?: unknown }).list
|
|
144
|
+
if (typeof maybeList !== 'function') {
|
|
145
|
+
markUnsupported('agent-transform-list')
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
const result = (maybeList as (this: unknown) => unknown).call(draft)
|
|
149
|
+
if (
|
|
150
|
+
result == null ||
|
|
151
|
+
typeof (result as { [Symbol.iterator]?: unknown })[Symbol.iterator] !== 'function'
|
|
152
|
+
) {
|
|
153
|
+
markUnsupported('agent-transform-list')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
agents = result as Iterable<{ id: string }>
|
|
157
|
+
} catch {
|
|
158
|
+
markUnsupported('agent-transform-list')
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
for (const agent of agents) {
|
|
163
|
+
draft.update(agent.id, (current) => {
|
|
164
|
+
// Beta 19192 hardening + SystemPart SKIP: `system` may be a non-string
|
|
165
|
+
// (host shape drift, e.g. object). `?.` covers only null/undefined, not
|
|
166
|
+
// wrong types — guard with typeof. String path preserved; empty/absent
|
|
167
|
+
// degrades to POLICY; truthy non-string OBJECT is SKIPPED (preserve host
|
|
168
|
+
// shape, avoid schema validation failure). Other primitives degrade to
|
|
169
|
+
// POLICY. Never throw.
|
|
170
|
+
try {
|
|
171
|
+
const sys: unknown = (current as { system?: unknown }).system
|
|
172
|
+
if (typeof sys === 'string' && sys.includes(POLICY_MARKER)) {
|
|
173
|
+
// Policy already present — nothing to do.
|
|
174
|
+
} else if (typeof sys === 'string' && sys) {
|
|
175
|
+
current.system = `${sys}\n\n${POLICY}`
|
|
176
|
+
} else if (sys == null || sys === '') {
|
|
177
|
+
current.system = POLICY
|
|
178
|
+
} else if (typeof sys === 'object') {
|
|
179
|
+
// Host object shape (e.g. SystemPart): SKIP instead of overwriting.
|
|
180
|
+
} else {
|
|
181
|
+
current.system = POLICY
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Fail-open: leave agent untouched on unexpected shape.
|
|
185
|
+
}
|
|
186
|
+
if (agent.id === 'zeus') current.mode = 'primary'
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
markUnsupported('agent-transform')
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function transformCatalog(draft: CatalogDraft, options: PluginContext['options']): void {
|
|
195
|
+
const configured = options.default_model
|
|
196
|
+
if (typeof configured !== 'string') return
|
|
197
|
+
const separator = configured.indexOf('/')
|
|
198
|
+
if (separator <= 0 || separator === configured.length - 1) return
|
|
199
|
+
const providerID = configured.slice(0, separator)
|
|
200
|
+
const modelID = configured.slice(separator + 1)
|
|
201
|
+
if (draft.model.get(providerID, modelID)) draft.model.default.set(providerID, modelID)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function transformCommands(draft: CommandDraft): void {
|
|
205
|
+
// Beta 19192 guard (issue #92): draft may not have `list`, or list()
|
|
206
|
+
// may return a non-iterable. Never throw — mark unsupported and return.
|
|
207
|
+
let commands: Iterable<{ name: string }>
|
|
208
|
+
try {
|
|
209
|
+
const maybeList = (draft as unknown as { list?: unknown }).list
|
|
210
|
+
if (typeof maybeList !== 'function') {
|
|
211
|
+
markUnsupported('command-transform-list')
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
const result = (maybeList as (this: unknown) => unknown).call(draft)
|
|
215
|
+
if (
|
|
216
|
+
result == null ||
|
|
217
|
+
typeof (result as { [Symbol.iterator]?: unknown })[Symbol.iterator] !== 'function'
|
|
218
|
+
) {
|
|
219
|
+
markUnsupported('command-transform-list')
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
commands = result as Iterable<{ name: string }>
|
|
223
|
+
} catch {
|
|
224
|
+
markUnsupported('command-transform-list')
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
for (const command of commands) {
|
|
229
|
+
if (command.name.startsWith('pantheon-')) {
|
|
230
|
+
draft.update(command.name, (current) => {
|
|
231
|
+
current.description ??= 'Pantheon orchestration command'
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
markUnsupported('command-transform')
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function transformSkills(draft: SkillDraft): void {
|
|
241
|
+
// Beta 19192 guard (issue #92): draft may not have `list`, or list()
|
|
242
|
+
// may return a non-iterable. Never throw — mark unsupported and return.
|
|
243
|
+
let sources: Array<{ type: string; path: string }>
|
|
244
|
+
try {
|
|
245
|
+
const maybeList = (draft as unknown as { list?: unknown }).list
|
|
246
|
+
if (typeof maybeList !== 'function') {
|
|
247
|
+
markUnsupported('skill-transform-list')
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
const result = (maybeList as (this: unknown) => unknown).call(draft)
|
|
251
|
+
if (
|
|
252
|
+
result == null ||
|
|
253
|
+
typeof (result as { [Symbol.iterator]?: unknown })[Symbol.iterator] !== 'function'
|
|
254
|
+
) {
|
|
255
|
+
markUnsupported('skill-transform-list')
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
sources = Array.isArray(result)
|
|
259
|
+
? (result as Array<{ type: string; path: string }>)
|
|
260
|
+
: Array.from(result as Iterable<{ type: string; path: string }>)
|
|
261
|
+
} catch {
|
|
262
|
+
markUnsupported('skill-transform-list')
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const path = fileURLToPath(new URL('./skills', import.meta.url))
|
|
267
|
+
if (!sources.some((source) => source.type === 'directory' && source.path === path)) {
|
|
268
|
+
draft.source({ type: 'directory', path })
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
markUnsupported('skill-transform')
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function transformReferences(draft: ReferenceDraft): void {
|
|
276
|
+
draft.add('pantheon-agents', {
|
|
277
|
+
type: 'local',
|
|
278
|
+
path: fileURLToPath(new URL('../AGENTS.md', import.meta.url)),
|
|
279
|
+
description: 'Pantheon agent and execution policy',
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── V2 Tool Registration ───────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Attempt to register Pantheon tools via V2 ctx.tool.transform.
|
|
287
|
+
*
|
|
288
|
+
* The V2 tool.transform API (when available) provides:
|
|
289
|
+
* draft.namespace({ name: "pantheon", description: "..." })
|
|
290
|
+
* draft.add({ name, description, input, execute })
|
|
291
|
+
*
|
|
292
|
+
* Returns true if tools were registered, false if the API is unavailable.
|
|
293
|
+
*/
|
|
294
|
+
async function registerV2Tools(context: PluginContext): Promise<boolean> {
|
|
295
|
+
const toolCtx = (context as unknown as Record<string, unknown>).tool as
|
|
296
|
+
| { transform?: (cb: (draft: V2ToolDraft) => void) => Promise<unknown> }
|
|
297
|
+
| undefined
|
|
298
|
+
|
|
299
|
+
if (!toolCtx?.transform) {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
// Tool definitions require infrastructure instances — when available via
|
|
305
|
+
// V2 context, they are passed through; otherwise the factory returns
|
|
306
|
+
// definitions with lazy execute wrappers.
|
|
307
|
+
const toolDefs = createV2ToolDefinitionsFromContext(context)
|
|
308
|
+
|
|
309
|
+
await toolCtx.transform((draft: V2ToolDraft) => {
|
|
310
|
+
draft.namespace({ name: 'pantheon', description: 'Pantheon orchestration tools' })
|
|
311
|
+
for (const def of toolDefs) {
|
|
312
|
+
draft.add({
|
|
313
|
+
name: def.name,
|
|
314
|
+
description: def.description,
|
|
315
|
+
input: def.input,
|
|
316
|
+
execute: def.execute,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
return true
|
|
321
|
+
} catch {
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ─── V2 Event Subscription ──────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Attempt to subscribe to session events via V2 ctx.event.subscribe.
|
|
330
|
+
*
|
|
331
|
+
* The V2 event.subscribe API (when available) provides an async iterable:
|
|
332
|
+
* for await (const event of ctx.event.subscribe({ signal })) { ... }
|
|
333
|
+
*
|
|
334
|
+
* Returns a cleanup function, or undefined if the API is unavailable.
|
|
335
|
+
*/
|
|
336
|
+
function subscribeV2Events(context: PluginContext): (() => void) | undefined {
|
|
337
|
+
const eventCtx = (context as unknown as Record<string, unknown>).event as
|
|
338
|
+
| { subscribe?: (opts?: { signal?: AbortSignal }) => AsyncIterable<V2SessionEvent> }
|
|
339
|
+
| undefined
|
|
340
|
+
|
|
341
|
+
if (!eventCtx?.subscribe) {
|
|
342
|
+
return undefined
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const controller = new AbortController()
|
|
346
|
+
void (async () => {
|
|
347
|
+
try {
|
|
348
|
+
// biome-ignore lint/style/noNonNullAssertion: subscribe guaranteed by guard above
|
|
349
|
+
for await (const event of eventCtx.subscribe!({ signal: controller.signal })) {
|
|
350
|
+
await handleV2SessionEvent(event)
|
|
351
|
+
}
|
|
352
|
+
} catch (err: unknown) {
|
|
353
|
+
if (err instanceof Error && err.name === 'AbortError') return
|
|
354
|
+
// Log but don't crash
|
|
355
|
+
console.warn('[Pantheon V2] Event subscription error:', err)
|
|
356
|
+
}
|
|
357
|
+
})()
|
|
358
|
+
|
|
359
|
+
return () => controller.abort()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Route a V2 session event to the appropriate handler.
|
|
364
|
+
*/
|
|
365
|
+
async function handleV2SessionEvent(event: V2SessionEvent): Promise<void> {
|
|
366
|
+
switch (event.type) {
|
|
367
|
+
case 'session.created':
|
|
368
|
+
await onSessionCreated(event)
|
|
369
|
+
break
|
|
370
|
+
case 'session.idle':
|
|
371
|
+
await onSessionIdle(event)
|
|
372
|
+
break
|
|
373
|
+
case 'session.error':
|
|
374
|
+
await onSessionError(event)
|
|
375
|
+
break
|
|
376
|
+
case 'session.compacted':
|
|
377
|
+
await onSessionCompacted(event)
|
|
378
|
+
break
|
|
379
|
+
default:
|
|
380
|
+
break
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function onSessionCreated(_event: V2SessionEvent): Promise<void> {
|
|
385
|
+
// Seed session state — handled by V1 bridge or direct registration.
|
|
386
|
+
// The V2 event doesn't carry enough context for full hierarchy setup;
|
|
387
|
+
// bridge.board is used when available for root-session registration.
|
|
388
|
+
const _bridge = resolveBridge()
|
|
389
|
+
if (_bridge?.board != null) {
|
|
390
|
+
// Bridge available: board will register root sessions via the V1 event hook.
|
|
391
|
+
// This is a no-op here — V1 handles hierarchy setup.
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function onSessionIdle(_event: V2SessionEvent): Promise<void> {
|
|
396
|
+
// Idle continuation — goal loop / todo enforcer dispatch.
|
|
397
|
+
// Bridge provides the full V1 infrastructure when available.
|
|
398
|
+
const sessionID = _event.properties?.sessionID as string | undefined
|
|
399
|
+
if (!sessionID) return
|
|
400
|
+
const bridge = resolveBridge()
|
|
401
|
+
if (bridge?.todoEnforcer != null) {
|
|
402
|
+
try {
|
|
403
|
+
await bridge.todoEnforcer.onIdle(sessionID)
|
|
404
|
+
} catch {
|
|
405
|
+
// Fail-open: idle continuation must never break the session.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function onSessionError(_event: V2SessionEvent): Promise<void> {
|
|
411
|
+
// Error handling — delegation finalize as error.
|
|
412
|
+
const bridge = resolveBridge()
|
|
413
|
+
if (bridge?.board != null) {
|
|
414
|
+
// Bridge available: delegation error handling is done by V1 event hook.
|
|
415
|
+
// V2 only logs — V1 finalizeDelegation handles the board transition.
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function onSessionCompacted(_event: V2SessionEvent): Promise<void> {
|
|
420
|
+
// Compaction restore — todo preserve / context rebuild.
|
|
421
|
+
const bridge = resolveBridge()
|
|
422
|
+
if (bridge?.todoEnforcer != null) {
|
|
423
|
+
// Bridge available: todo preservation is done by V1 compaction hook.
|
|
424
|
+
// V2 only logs — V1 handles full compaction context build.
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─── V2 Session Hooks ───────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Register session hooks via V2 ctx.session.hook.
|
|
432
|
+
*
|
|
433
|
+
* The V2 session.hook API (when available) provides:
|
|
434
|
+
* ctx.session.hook("prompt", handler) — message admission
|
|
435
|
+
* ctx.session.hook("context", handler) — system context injection
|
|
436
|
+
*
|
|
437
|
+
* Returns true if any hooks were registered.
|
|
438
|
+
*/
|
|
439
|
+
async function registerV2SessionHooks(context: PluginContext): Promise<boolean> {
|
|
440
|
+
const sessionCtx = (context as unknown as Record<string, unknown>).session as
|
|
441
|
+
| {
|
|
442
|
+
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
443
|
+
}
|
|
444
|
+
| undefined
|
|
445
|
+
|
|
446
|
+
if (!sessionCtx?.hook) {
|
|
447
|
+
return false
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let registered = false
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
// "context" hook — inject routing policy + compaction state
|
|
454
|
+
await sessionCtx.hook('context', (event: unknown) => {
|
|
455
|
+
try {
|
|
456
|
+
// Beta 19192 hardening + SystemPart sniffing: host may send string[],
|
|
457
|
+
// SystemPart objects ({type,text}), mixed arrays, a single string, or
|
|
458
|
+
// a non-array system. Preserve string-path behavior; inject object
|
|
459
|
+
// shape when the array contains objects (else `Schema validation
|
|
460
|
+
// failed` in system[N]). Never throw to the host (fail-open).
|
|
461
|
+
const ctx = event as { system?: unknown } | null | undefined
|
|
462
|
+
const rawSystem: unknown = ctx?.system
|
|
463
|
+
if (typeof rawSystem === 'string' && ctx != null) {
|
|
464
|
+
ctx.system = rawSystem.includes(POLICY_MARKER) ? [rawSystem] : [rawSystem, POLICY]
|
|
465
|
+
} else if (Array.isArray(rawSystem)) {
|
|
466
|
+
if (!hasPolicyMarker(rawSystem)) {
|
|
467
|
+
rawSystem.push(toSystemEntry(POLICY, usesObjectShape(rawSystem)))
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Non-string, non-array system (or null event): ignore silently.
|
|
471
|
+
} catch {
|
|
472
|
+
// Fail-open: never throw to the host.
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
registered = true
|
|
476
|
+
} catch {
|
|
477
|
+
// Context hook not supported
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
// "prompt" hook — vision message interception
|
|
482
|
+
await sessionCtx.hook('prompt', (_event: unknown) => {
|
|
483
|
+
// Vision interception delegates to the V1 vision handler.
|
|
484
|
+
// Without V1 client access, this is a graceful no-op.
|
|
485
|
+
})
|
|
486
|
+
registered = true
|
|
487
|
+
} catch {
|
|
488
|
+
// Prompt hook not supported
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return registered
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── V2 Tool Hooks ──────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Register tool execution hooks via V2 ctx.tool.hook.
|
|
498
|
+
*
|
|
499
|
+
* The V2 tool.hook API (when available) provides:
|
|
500
|
+
* ctx.tool.hook("execute.before", handler) — pre-execution guard
|
|
501
|
+
* ctx.tool.hook("execute.after", handler) — post-execution augment
|
|
502
|
+
*
|
|
503
|
+
* Returns true if any hooks were registered.
|
|
504
|
+
*/
|
|
505
|
+
async function registerV2ToolHooks(context: PluginContext): Promise<boolean> {
|
|
506
|
+
const toolCtx = (context as unknown as Record<string, unknown>).tool as
|
|
507
|
+
| {
|
|
508
|
+
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
509
|
+
}
|
|
510
|
+
| undefined
|
|
511
|
+
|
|
512
|
+
if (!toolCtx?.hook) {
|
|
513
|
+
return false
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let registered = false
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
// "execute.before" — read-only enforcement + command normalization
|
|
520
|
+
await toolCtx.hook('execute.before', (_event: unknown) => {
|
|
521
|
+
// Enforcement guard runs via V1 bridge when infrastructure is available.
|
|
522
|
+
// The V2 hook is a registration point; actual enforcement is wired
|
|
523
|
+
// through the V1 tool.execute.before hook.
|
|
524
|
+
const bridge = resolveBridge()
|
|
525
|
+
if (bridge?.board != null) {
|
|
526
|
+
// Bridge available: V1 enforcement guard handles read-only + command normalization.
|
|
527
|
+
// No additional V2-side enforcement needed.
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
registered = true
|
|
531
|
+
} catch {
|
|
532
|
+
// Tool before-hook not supported
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
// "execute.after" — hashline read enhance + context sandbox
|
|
537
|
+
await toolCtx.hook('execute.after', (_event: unknown) => {
|
|
538
|
+
// Read enhancer + sandbox run via V1 bridge.
|
|
539
|
+
const bridge = resolveBridge()
|
|
540
|
+
if (bridge?.visionHandler != null) {
|
|
541
|
+
// Bridge available: V1 vision handler + read enhancer handle post-execution.
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
registered = true
|
|
545
|
+
} catch {
|
|
546
|
+
// Tool after-hook not supported
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return registered
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ─── V2 Permission Hook ─────────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Register permission hook via V2 ctx.permission.hook.
|
|
556
|
+
*
|
|
557
|
+
* The V2 permission.hook API (when available) provides:
|
|
558
|
+
* ctx.permission.hook("evaluate", handler) — custom permission logic
|
|
559
|
+
*
|
|
560
|
+
* Returns true if the hook was registered.
|
|
561
|
+
*/
|
|
562
|
+
async function registerV2PermissionHook(context: PluginContext): Promise<boolean> {
|
|
563
|
+
const permCtx = (context as unknown as Record<string, unknown>).permission as
|
|
564
|
+
| {
|
|
565
|
+
hook?: (name: string, handler: (event: unknown) => void | Promise<void>) => Promise<unknown>
|
|
566
|
+
}
|
|
567
|
+
| undefined
|
|
568
|
+
|
|
569
|
+
if (!permCtx?.hook) {
|
|
570
|
+
markUnsupported('permission-hook')
|
|
571
|
+
return false
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
await permCtx.hook('evaluate', (_event: unknown) => {
|
|
576
|
+
// Custom permission logic — Zeus read guard + read-only enforcement.
|
|
577
|
+
// Wired through V1 tool.execute.before when infrastructure is available.
|
|
578
|
+
})
|
|
579
|
+
return true
|
|
580
|
+
} catch {
|
|
581
|
+
markUnsupported('permission-hook')
|
|
582
|
+
return false
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ─── Tool Definition Factory ─────────────────────────────────────────────
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Internal types for V2 tool draft.
|
|
590
|
+
*/
|
|
591
|
+
interface V2ToolDraft {
|
|
592
|
+
namespace(config: { name: string; description: string }): void
|
|
593
|
+
add(tool: {
|
|
594
|
+
name: string
|
|
595
|
+
description: string
|
|
596
|
+
input: Record<string, unknown>
|
|
597
|
+
execute: (input: Record<string, unknown>, context: unknown) => Promise<V2ToolResult>
|
|
598
|
+
}): void
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* V2 tool result shape.
|
|
603
|
+
*
|
|
604
|
+
* The V1 SDK types `ToolResult` as `string | { output: string, ... }`, but
|
|
605
|
+
* the V2 beta host reads a field off the resolved value (minified `Ce`) and
|
|
606
|
+
* throws `Ce is not an Object` when a tool resolves to a bare string. Our
|
|
607
|
+
* handlers are fail-open, so that host-side TypeError escapes instead of
|
|
608
|
+
* being contained. Always resolve to the object shape — it satisfies both
|
|
609
|
+
* the SDK union and the beta host.
|
|
610
|
+
*/
|
|
611
|
+
export interface V2ToolResult {
|
|
612
|
+
output: string
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Wrap raw tool text into the object shape the V2 beta host requires.
|
|
617
|
+
* Never throws (fail-open): non-string input degrades to String(input).
|
|
618
|
+
*/
|
|
619
|
+
export function toV2ToolResult(text: unknown): V2ToolResult {
|
|
620
|
+
try {
|
|
621
|
+
return { output: typeof text === 'string' ? text : String(text) }
|
|
622
|
+
} catch {
|
|
623
|
+
return { output: '[pantheon] tool output unavailable' }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* A V2 tool definition ready for registration.
|
|
629
|
+
*/
|
|
630
|
+
interface V2ToolDef {
|
|
631
|
+
name: string
|
|
632
|
+
description: string
|
|
633
|
+
input: Record<string, unknown>
|
|
634
|
+
execute: (input: Record<string, unknown>, context: unknown) => Promise<V2ToolResult>
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Create V2 tool definitions from the plugin context.
|
|
639
|
+
*
|
|
640
|
+
* When V1 infrastructure (BackgroundJobBoard, delegation client, etc.) is
|
|
641
|
+
* available through the V2 context, tools are wired directly. Otherwise,
|
|
642
|
+
* each tool returns an error message explaining the limitation.
|
|
643
|
+
*/
|
|
644
|
+
function createV2ToolDefinitionsFromContext(_context: PluginContext): V2ToolDef[] {
|
|
645
|
+
// Tool definitions are created lazily. When the full V1 infrastructure
|
|
646
|
+
// is bridged through the V2 context, these can be wired to real implementations.
|
|
647
|
+
// For now, they are structural definitions with placeholder execute functions
|
|
648
|
+
// that explain the V2 limitation.
|
|
649
|
+
const v2UnavailableMessage =
|
|
650
|
+
'This tool requires V1 infrastructure (BackgroundJobBoard, SDK client). ' +
|
|
651
|
+
'Use the V1 plugin (src/plugin.ts) for full tool support, or wire V1 ' +
|
|
652
|
+
'infrastructure through the V2 bridge (src/pantheon/v2-bridge.ts).'
|
|
653
|
+
|
|
654
|
+
const toolNames = [
|
|
655
|
+
'pantheon_delegate',
|
|
656
|
+
'pantheon_delegation_read',
|
|
657
|
+
'pantheon_delegation_list',
|
|
658
|
+
'hashline_edit',
|
|
659
|
+
'pantheon_goal_create',
|
|
660
|
+
'pantheon_goal_get',
|
|
661
|
+
'pantheon_goal_update',
|
|
662
|
+
'pantheon_cost',
|
|
663
|
+
'pantheon_model',
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
const toolDescriptions: Record<string, string> = {
|
|
667
|
+
pantheon_delegate:
|
|
668
|
+
'Dispatch a background agent as a child session and register it on the job board. ' +
|
|
669
|
+
'Returns the readable alias (e.g. "apo-1"); read the result with pantheon_delegation_read.',
|
|
670
|
+
pantheon_delegation_read:
|
|
671
|
+
'Block until a background delegation finishes (completed/error/cancelled), then return ' +
|
|
672
|
+
'its report markdown (with a trailing agent-activity section) and mark the job reconciled.',
|
|
673
|
+
pantheon_delegation_list:
|
|
674
|
+
'List background delegations for the current session, with [unread] for finished jobs.',
|
|
675
|
+
hashline_edit:
|
|
676
|
+
'Edit a file anchored by hashline refs (LINE#TAG) instead of raw line numbers. ' +
|
|
677
|
+
'Ops: replace, append, prepend, delete. ALL refs validated against ORIGINAL file first.',
|
|
678
|
+
pantheon_goal_create:
|
|
679
|
+
'Create the single active goal for this session. The goal loop then continues the ' +
|
|
680
|
+
'session automatically on idle until the goal is marked done.',
|
|
681
|
+
pantheon_goal_get:
|
|
682
|
+
'Return the active goal for this session (status, objective, continuations).',
|
|
683
|
+
pantheon_goal_update:
|
|
684
|
+
'Update the active goal: set status (pending/in_progress/done) and/or rewrite the objective. ' +
|
|
685
|
+
'Setting status to done stops all further continuations.',
|
|
686
|
+
pantheon_cost:
|
|
687
|
+
'Report delegation cost + token usage by agent over the last N days, read from opencode.db.',
|
|
688
|
+
pantheon_model:
|
|
689
|
+
'Show, set, or reset per-agent model overrides in active-preset.json (project or global).',
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return toolNames.map((name) => ({
|
|
693
|
+
name,
|
|
694
|
+
description: toolDescriptions[name] ?? `Pantheon tool: ${name}`,
|
|
695
|
+
input: { type: 'object' as const, properties: {} },
|
|
696
|
+
// FOLLOW-UP: connect the real wrappers from
|
|
697
|
+
// src/pantheon/v2-tool-definitions.ts (createV2ToolDefinitions with
|
|
698
|
+
// costCommand/goalTools) once V1 infrastructure is resolvable in the V2
|
|
699
|
+
// standalone context. The before/after factories in v2-hooks.ts:216-282
|
|
700
|
+
// are likewise unwired. Until then, placeholders resolve to the object
|
|
701
|
+
// shape via toV2ToolResult so the beta host never sees a bare string.
|
|
702
|
+
execute: async (_input: Record<string, unknown>, _context: unknown): Promise<V2ToolResult> => {
|
|
703
|
+
return toV2ToolResult(`${name}: ${v2UnavailableMessage}`)
|
|
704
|
+
},
|
|
705
|
+
}))
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ─── Plugin Definition ───────────────────────────────────────────────────
|
|
709
|
+
|
|
710
|
+
/** Event type from V2 event stream. */
|
|
711
|
+
interface V2SessionEvent {
|
|
712
|
+
type: string
|
|
713
|
+
properties?: Record<string, unknown>
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** Cleanup function type. */
|
|
717
|
+
type CleanupFn = () => void
|
|
718
|
+
|
|
719
|
+
/** Active cleanup handlers for the plugin lifecycle. */
|
|
720
|
+
const activeCleanups: CleanupFn[] = []
|
|
721
|
+
|
|
722
|
+
export const plugin = define({
|
|
723
|
+
id: 'pantheon-opencode-v2',
|
|
724
|
+
|
|
725
|
+
async setup(context: PluginContext): Promise<void> {
|
|
726
|
+
// ─── Phase 1: V2 Transforms (isolated per-domain, best-effort) ────
|
|
727
|
+
// Each domain registers independently: a failure in one transform
|
|
728
|
+
// (e.g. draft.list missing on beta 19192 → TypeError) must never
|
|
729
|
+
// reject setup() nor prevent the other domains from registering.
|
|
730
|
+
// See issue #92.
|
|
731
|
+
const phase1Transforms: Array<{ feature: string; register: () => Promise<unknown> }> = [
|
|
732
|
+
{ feature: 'agent-transform', register: () => context.agent.transform(transformAgents) },
|
|
733
|
+
{
|
|
734
|
+
feature: 'catalog-transform',
|
|
735
|
+
register: () =>
|
|
736
|
+
context.catalog.transform((draft) => transformCatalog(draft, context.options)),
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
feature: 'command-transform',
|
|
740
|
+
register: () => context.command.transform(transformCommands),
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
feature: 'reference-transform',
|
|
744
|
+
register: () => context.reference.transform(transformReferences),
|
|
745
|
+
},
|
|
746
|
+
{ feature: 'skill-transform', register: () => context.skill.transform(transformSkills) },
|
|
747
|
+
]
|
|
748
|
+
const phase1Results = await Promise.allSettled(
|
|
749
|
+
phase1Transforms.map((t) => Promise.resolve().then(() => t.register())),
|
|
750
|
+
)
|
|
751
|
+
phase1Results.forEach((result, index) => {
|
|
752
|
+
// biome-ignore lint/style/noNonNullAssertion: index always in bounds — parallel arrays
|
|
753
|
+
const feature = phase1Transforms[index]!.feature
|
|
754
|
+
if (result.status === 'rejected') {
|
|
755
|
+
markUnsupported(feature)
|
|
756
|
+
}
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
// ─── Phase 2: V2 Tool Registration (best-effort) ─────────────────
|
|
760
|
+
const toolsRegistered = await registerV2Tools(context).catch(() => false)
|
|
761
|
+
if (!toolsRegistered) {
|
|
762
|
+
markUnsupported('tool-transform')
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ─── Phase 3: V2 Event Subscription (best-effort) ────────────────
|
|
766
|
+
const eventCleanup = subscribeV2Events(context)
|
|
767
|
+
if (eventCleanup) {
|
|
768
|
+
activeCleanups.push(eventCleanup)
|
|
769
|
+
} else {
|
|
770
|
+
markUnsupported('event-stream')
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ─── Phase 4: V2 Session Hooks (best-effort) ─────────────────────
|
|
774
|
+
const sessionHooksRegistered = await registerV2SessionHooks(context).catch(() => false)
|
|
775
|
+
if (!sessionHooksRegistered) {
|
|
776
|
+
markUnsupported('session-hooks')
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ─── Phase 5: V2 Tool Hooks (best-effort) ────────────────────────
|
|
780
|
+
const toolHooksRegistered = await registerV2ToolHooks(context).catch(() => false)
|
|
781
|
+
if (!toolHooksRegistered) {
|
|
782
|
+
markUnsupported('tool-execute-hooks')
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ─── Phase 6: V2 Permission Hook (best-effort) ───────────────────
|
|
786
|
+
await registerV2PermissionHook(context).catch(() => false)
|
|
787
|
+
|
|
788
|
+
// ─── Phase 7: V2 Compaction Hook (best-effort) ───────────────────
|
|
789
|
+
const compactionCtx = (context as unknown as Record<string, unknown>).session as
|
|
790
|
+
| {
|
|
791
|
+
hook?: (
|
|
792
|
+
name: string,
|
|
793
|
+
handler: (event: unknown) => void | Promise<void>,
|
|
794
|
+
) => Promise<unknown>
|
|
795
|
+
}
|
|
796
|
+
| undefined
|
|
797
|
+
if (compactionCtx?.hook) {
|
|
798
|
+
try {
|
|
799
|
+
await compactionCtx.hook('compacting', (_event: unknown) => {
|
|
800
|
+
// Compaction context build — injects active goals, pending todos,
|
|
801
|
+
// and in-flight delegations. Wired through V1 infrastructure.
|
|
802
|
+
})
|
|
803
|
+
} catch {
|
|
804
|
+
markUnsupported('compaction-hook')
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
markUnsupported('compaction-hook')
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
// ─── V1 Compatibility Bridge ─────────────────────────────────────────────
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* V1-compatible cleanup — call from the V1 plugin's dispose hook or
|
|
816
|
+
* process exit handler to clean up V2 event subscriptions.
|
|
817
|
+
*/
|
|
818
|
+
export function v2Dispose(): void {
|
|
819
|
+
for (const cleanup of activeCleanups) {
|
|
820
|
+
try {
|
|
821
|
+
cleanup()
|
|
822
|
+
} catch {
|
|
823
|
+
// Best-effort cleanup
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
activeCleanups.length = 0
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Get the current list of unsupported V2 features (for diagnostics).
|
|
831
|
+
*/
|
|
832
|
+
export function getUnsupportedFeatures(): readonly string[] {
|
|
833
|
+
return V2_UNSUPPORTED_FEATURES
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export default plugin
|