pantheon-opencode 1.2.0 → 1.2.2

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 (63) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +112 -15
  3. package/bin/pantheon-init.mjs +93 -0
  4. package/commands/pantheon-vision.md +67 -0
  5. package/opencode.json +14 -10
  6. package/package.json +20 -4
  7. package/scripts/_pantheon_paths.py +0 -2
  8. package/scripts/changelog-extract.mjs +112 -0
  9. package/scripts/doctor.mjs +307 -15
  10. package/scripts/hooks/audit-imports.sh +43 -0
  11. package/scripts/hooks/format-multi-language.sh +96 -0
  12. package/scripts/hooks/log-session-start.sh +24 -0
  13. package/scripts/hooks/on-subagent-delegation-start.sh +91 -0
  14. package/scripts/hooks/on-subagent-delegation-stop.sh +37 -0
  15. package/scripts/hooks/run-type-check.sh +21 -0
  16. package/scripts/hooks/scan-secrets.sh +55 -0
  17. package/scripts/hooks/validate-post-conditions.sh +21 -0
  18. package/scripts/hooks/validate-talos-scope.sh +43 -0
  19. package/scripts/hooks/validate-tool-safety.sh +46 -0
  20. package/scripts/install/health-check.mjs +101 -1
  21. package/scripts/install/model-picker.mjs +118 -0
  22. package/scripts/install/opencode.mjs +155 -43
  23. package/scripts/install/plugin.mjs +4 -2
  24. package/scripts/install/venv.mjs +25 -2
  25. package/scripts/install-mcp.mjs +170 -37
  26. package/scripts/mcp_persistence_server.py +2 -1
  27. package/scripts/prune.mjs +359 -0
  28. package/scripts/secret-scan.mjs +88 -0
  29. package/scripts/uninstall.mjs +3 -3
  30. package/scripts/validate-routing.mjs +15 -0
  31. package/scripts/version-check.mjs +146 -0
  32. package/scripts/versioning.mjs +13 -14
  33. package/src/agents/hermes.md +2 -2
  34. package/src/agents/mnemosyne.md +1 -1
  35. package/src/agents/zeus.md +1 -1
  36. package/src/mcp/_pantheon_paths.py +0 -2
  37. package/src/mcp/install-mcp.mjs +6 -913
  38. package/src/mcp/mcp_persistence_server.py +2 -1
  39. package/src/mcp/pantheon_vision_server.py +495 -0
  40. package/src/mcp/requirements-mcp.txt +8 -5
  41. package/src/mcp/requirements-vision.txt +3 -0
  42. package/src/pantheon/auto-wake.ts +91 -0
  43. package/src/pantheon/background-job-board.ts +568 -0
  44. package/src/pantheon/file-persistence.ts +95 -0
  45. package/src/pantheon/presets.d.mts +108 -0
  46. package/src/pantheon/presets.mjs +554 -0
  47. package/src/pantheon/vision.ts +1437 -0
  48. package/src/plugin.ts +69 -25
  49. package/src/plugins/hook-runner.ts +186 -0
  50. package/src/plugins/pantheon-hooks.ts +153 -0
  51. package/src/plugins/tui/dist/tui.d.ts +1 -1
  52. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  53. package/src/plugins/tui/dist/tui.js +812 -223
  54. package/src/plugins/tui/dist/tui.js.map +1 -1
  55. package/src/plugins/tui/dist/tui.tsx +881 -121
  56. package/src/plugins/tui/package.json +3 -3
  57. package/src/plugins/tui/src/index.tsx +881 -121
  58. package/src/routing.yml +148 -0
  59. package/src/skills/context-compression/SKILL.md +1 -1
  60. package/src/skills/loop-engineering/SKILL.md +1 -1
  61. package/src/skills/orchestration-workflow/SKILL.md +11 -11
  62. package/scripts/scrub_secrets.py +0 -281
  63. package/src/mcp/requirements-mcp-core.txt +0 -5
package/src/plugin.ts CHANGED
@@ -1,6 +1,9 @@
1
+ import type { Plugin, PluginInput } from '@opencode-ai/plugin'
1
2
  import type { PluginConfig } from 'opencode'
2
3
  import { BackgroundJobBoard } from './pantheon/background-job-board.ts'
3
4
  import { FilePersistenceAdapter } from './pantheon/file-persistence.ts'
5
+ import { applyActivePresetToConfig } from './pantheon/presets.mjs'
6
+ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.ts'
4
7
 
5
8
  // ─── Background Job Board Singleton ────────────────────────────────────
6
9
 
@@ -8,17 +11,11 @@ const board = new BackgroundJobBoard({
8
11
  maxConcurrentPerAgent: 3,
9
12
  signalDir: '.pantheon/deepwork/board-signals',
10
13
  })
11
-
12
- // Wire up file-based persistence and recover orphaned jobs from crashes
13
14
  const persistence = new FilePersistenceAdapter('.pantheon/board/state.json')
14
15
  board.setPersistence(persistence)
15
-
16
- // Recover on startup — any jobs left in "running" state become "error"
17
- board.recoverRunningJobs().catch((err) =>
18
- console.error('[Pantheon Plugin] Failed to recover running jobs:', err),
19
- )
20
-
21
- // Log board state changes to console for observability
16
+ board
17
+ .recoverRunningJobs()
18
+ .catch((err) => console.error('[Pantheon Plugin] Failed to recover running jobs:', err))
22
19
  board.onTerminal((taskID: string) => {
23
20
  const job = board.get(taskID)
24
21
  if (job) {
@@ -29,29 +26,76 @@ board.onTerminal((taskID: string) => {
29
26
  })
30
27
 
31
28
  /**
32
- * Get the global BackgroundJobBoard singleton.
29
+ * Pantheon plugin for OpenCode. Pasted images are intercepted via the
30
+ * `chat.message` hook (proven to fire in opencode 1.18.11). When a provider
31
+ * key is available — env PANTHEON_OPENCODE_API_KEY / OPENCODE_API_KEY, or the
32
+ * opencode auth store for `opencode auth login` users — the image is
33
+ * described NATIVELY by the multimodal model via the opencode Zen
34
+ * OpenAI-compatible endpoint, and replaced with the text description — no MCP
35
+ * tool required. Without a key the legacy pattern applies: the image is
36
+ * replaced with a text instruction telling the model to call a vision MCP tool
37
+ * (default `pantheon_vision_vision_describe`). Either way the image never reaches
38
+ * the main provider, so text-only models cannot fail with an `image_url` error.
33
39
  *
34
- * All modules (Zeus, auto-wake, etc.) should access the board through this
35
- * function to ensure a single consistent state machine across the plugin.
40
+ * The canonical `pantheon-vision` MCP owns the standalone describe/OCR/analyze
41
+ * tools. The installed OpenCode plugin API exposes no stable pre-provider
42
+ * message hook (1.18.11 only exposes the experimental history transform), so
43
+ * that transform is retained as the runtime-compatible fallback. If a stable
44
+ * provider-bound hook is added, register the same sanitizer there instead of
45
+ * assuming this experimental hook is guaranteed to run.
46
+ *
47
+ * IMPORTANT (OpenCode 1.18.11 legacy loader): this module must export EXACTLY
48
+ * ONE function-valued export — the default plugin. The legacy loader does
49
+ * `Object.values(mod)` and invokes every function export as a plugin factory;
50
+ * any named function export (e.g. a re-exported helper like
51
+ * generateInjectionPrompt) is called with a PluginInput object and can throw.
52
+ * Helpers live in src/pantheon/vision.ts and are imported from there directly.
36
53
  */
37
- export function getBackgroundJobBoard(): BackgroundJobBoard {
38
- return board
39
- }
54
+ const plugin: Plugin = async (input: PluginInput) => {
55
+ const vision = createVisionHandler(input)
40
56
 
41
- export default {
42
- name: 'pantheon',
43
- version: '5.0.0',
44
- description: 'Pantheon multi-agent orchestration platform',
45
-
46
- hooks: {
57
+ return {
47
58
  config: async (config: PluginConfig) => {
48
59
  config.agentsPath = config.agentsPath ?? []
49
60
  config.agentsPath.push(new URL('./agents', import.meta.url).pathname)
50
-
51
61
  config.skillsPaths = config.skillsPaths ?? []
52
62
  config.skillsPaths.push(new URL('./skills', import.meta.url).pathname)
53
63
 
54
- return config
64
+ // Apply the active model preset (`init --preset` / `set-tier` write
65
+ // .pantheon/active-preset.json): resolve it with the SAME candidate
66
+ // order the vision handler uses (project > XDG > HOME) and mutate the
67
+ // agent models / reasoning effort / fallback models + provider configs.
68
+ // Fail-safe: without an active preset the config is untouched; a missing
69
+ // provider key is logged and skipped — the hook must never break startup
70
+ // (set-tier already fail-fast validates keys at write time). Vision
71
+ // rotation stays in vision.ts, which resolves its model independently.
72
+ try {
73
+ const resolved = applyActivePresetToConfig(config, {
74
+ candidates: activePresetCandidates(),
75
+ })
76
+ if (resolved) {
77
+ console.log(
78
+ `[Pantheon Plugin] Applied model preset: ${resolved.name} (${resolved.source})`,
79
+ )
80
+ }
81
+ } catch {
82
+ // Fully STATIC warning — the thrown error object is tainted because
83
+ // presets.mjs constructs it with the provider's apiKeyEnv name (e.g.
84
+ // PANTHEON_OPENCODE_API_KEY), and CodeQL's clear-text logging dataflow
85
+ // flags ANY interpolation derived from it — including err.name and
86
+ // err.message. The env var name is not itself sensitive to log, but
87
+ // the scanner cannot distinguish it, so nothing derived from the error
88
+ // may appear here. Diagnostics (provider id + env var) are already
89
+ // emitted by applyActivePresetToConfig's own logger before it throws.
90
+ // The catch binding is intentionally omitted so no tainted value can
91
+ // ever reach this log line (CodeQL alert #11).
92
+ console.warn('[plugin] preset application failed (see logs for details)')
93
+ }
55
94
  },
56
- },
57
- } satisfies PluginConfig
95
+ 'chat.message': vision.chatMessage,
96
+ 'experimental.chat.messages.transform': vision.messagesTransform,
97
+ event: vision.event,
98
+ }
99
+ }
100
+
101
+ export default plugin
@@ -0,0 +1,186 @@
1
+ /**
2
+ * hook-runner.ts — version-proof runner for the Pantheon security-hook layer.
3
+ *
4
+ * The previous plugin used the Bun Shell `$` injected by the opencode plugin
5
+ * API (`$.quiet().nothrow().timeout(...)`). `.timeout()` does not exist on the
6
+ * BunShell interface, so every hook crashed with a TypeError and the layer was
7
+ * removed (24-25/07). This runner uses ONLY `node:child_process` — stable
8
+ * across every Node >= 18 runtime and every opencode plugin loader version.
9
+ *
10
+ * Claude Code stdin protocol: the hook scripts receive
11
+ * `{tool_name, tool_input, agent_id, session_id}` as JSON on stdin. Scripts
12
+ * that read env vars instead (log-session-start.sh, on-subagent-delegation-*)
13
+ * get their fields mapped into the child env (SESSION_ID / AGENT_NAME /
14
+ * TASK_DESC) so both protocols work end to end.
15
+ *
16
+ * IMPORTANT (verified empirically): the `input:` option on the async
17
+ * `execFile()` API is silently ignored by Node — it only exists on
18
+ * spawnSync/execFileSync. This runner therefore writes stdin explicitly to a
19
+ * spawned child, which delivers the payload reliably.
20
+ *
21
+ * Hooks NEVER throw: every failure (missing script, timeout, non-zero exit)
22
+ * is returned as a structured HookResult so the opencode session can never be
23
+ * taken down by the security layer.
24
+ */
25
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
26
+ import path from 'node:path'
27
+ import type { Readable } from 'node:stream'
28
+ import { fileURLToPath } from 'node:url'
29
+
30
+ export const HOOK_TIMEOUT_MS = 15_000
31
+ export const HOOK_MAX_BUFFER = 10 * 1024 * 1024
32
+
33
+ /** Claude Code hook protocol payload. */
34
+ export type HookPayload = {
35
+ tool_name?: string
36
+ tool_input?: unknown
37
+ agent_id?: string
38
+ session_id?: string
39
+ }
40
+
41
+ export type HookResult = {
42
+ code: number
43
+ stdout: string
44
+ stderr: string
45
+ signal: NodeJS.Signals | null
46
+ timedOut: boolean
47
+ }
48
+
49
+ export type RunHookOptions = {
50
+ env?: Record<string, string | undefined>
51
+ cwd?: string
52
+ timeout?: number
53
+ }
54
+
55
+ /**
56
+ * Resolve the hooks directory relative to this module — robust to any npm
57
+ * global-install prefix because it anchors on import.meta.url, not __dirname
58
+ * or process.cwd().
59
+ */
60
+ export function resolveHooksDir(): string {
61
+ return fileURLToPath(new URL('../../scripts/hooks/', import.meta.url))
62
+ }
63
+
64
+ /**
65
+ * Map the Claude Code protocol payload into the child environment. Scripts
66
+ * that read env vars (log-session-start.sh, delegation audit scripts) rely on
67
+ * these; scripts that read stdin receive the full JSON via `input` in runHook.
68
+ * PATH is always preserved so `python3`/`bash` resolution inside the scripts
69
+ * is never affected by a stripped-down environment.
70
+ */
71
+ export function hookEnv(
72
+ payload: HookPayload,
73
+ extra?: Record<string, string | undefined>,
74
+ ): NodeJS.ProcessEnv {
75
+ return {
76
+ ...process.env,
77
+ PATH: process.env.PATH ?? '',
78
+ SESSION_ID: payload.session_id ?? '',
79
+ AGENT_NAME: payload.agent_id ?? '',
80
+ TASK_DESC: payload.tool_name ?? '',
81
+ ...(extra ?? {}),
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Run one hook script with the Claude Code protocol payload on stdin.
87
+ *
88
+ * @param script file name inside scripts/hooks/ (e.g. "validate-tool-safety.sh")
89
+ * @param payload protocol payload translated from the opencode hook event
90
+ * @param opts optional env overrides / cwd / timeout
91
+ * @returns HookResult — NEVER throws; resolve with a non-zero code instead
92
+ */
93
+ export function runHook(
94
+ script: string,
95
+ payload: HookPayload,
96
+ opts: RunHookOptions = {},
97
+ ): Promise<HookResult> {
98
+ const scriptPath = path.join(opts.cwd ?? resolveHooksDir(), script)
99
+ const timeout = opts.timeout ?? HOOK_TIMEOUT_MS
100
+
101
+ return new Promise<HookResult>((resolve) => {
102
+ const stdoutBuf: { value: string } = { value: '' }
103
+ const stderrBuf: { value: string } = { value: '' }
104
+ let settled = false
105
+
106
+ const settle = (result: HookResult) => {
107
+ if (!settled) {
108
+ settled = true
109
+ resolve(result)
110
+ }
111
+ }
112
+
113
+ const spawnOpts = {
114
+ timeout,
115
+ killSignal: 'SIGKILL' as const,
116
+ env: hookEnv(payload, opts.env),
117
+ // stdio:'pipe' is equivalent to ['pipe','pipe','pipe'] — it guarantees
118
+ // the child's stdin is OUR pipe, NEVER the opencode TUI's terminal
119
+ // (which would leave `cat`-style hooks blocked reading forever and hit
120
+ // the opencode core hook timeout).
121
+ stdio: 'pipe' as const,
122
+ windowsHide: true,
123
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
124
+ }
125
+
126
+ // stdio:'pipe' picks the ChildProcessWithoutNullStreams overload — do not
127
+ // widen these option literals or child.stdin/stdout become nullable.
128
+ let child: ChildProcessWithoutNullStreams
129
+ try {
130
+ child = spawn(scriptPath, [], spawnOpts)
131
+ } catch (err) {
132
+ // Synchronous spawn failure (invalid args etc.) — never propagate.
133
+ settle({
134
+ code: 1,
135
+ stdout: '',
136
+ stderr: `spawn failed: ${err instanceof Error ? err.message : String(err)}`,
137
+ signal: null,
138
+ timedOut: false,
139
+ })
140
+ return
141
+ }
142
+
143
+ // The child may exit before we write stdin — swallow EPIPE instead of
144
+ // crashing the plugin process with an unhandled stream error.
145
+ child.stdin.on('error', () => {})
146
+
147
+ const collect = (stream: Readable, buf: { value: string }) => {
148
+ stream.setEncoding('utf-8')
149
+ stream.on('data', (chunk: string) => {
150
+ if (buf.value.length < HOOK_MAX_BUFFER) {
151
+ buf.value += chunk.slice(0, HOOK_MAX_BUFFER - buf.value.length)
152
+ }
153
+ })
154
+ }
155
+ collect(child.stdout, stdoutBuf)
156
+ collect(child.stderr, stderrBuf)
157
+
158
+ child.on('error', (err: NodeJS.ErrnoException) => {
159
+ // ENOENT / EACCES — script missing or not executable. Report, never throw.
160
+ settle({
161
+ code: 1,
162
+ stdout: stdoutBuf.value,
163
+ stderr: stderrBuf.value || `hook script failed to spawn: ${err.message}`,
164
+ signal: null,
165
+ timedOut: false,
166
+ })
167
+ })
168
+
169
+ child.on('close', (code, signal) => {
170
+ settle({
171
+ code: code ?? 1,
172
+ stdout: stdoutBuf.value,
173
+ stderr: stderrBuf.value,
174
+ signal,
175
+ timedOut: code === null && signal === 'SIGKILL',
176
+ })
177
+ })
178
+
179
+ // Deliver the Claude Code protocol JSON on stdin. stdin.end() runs
180
+ // UNCONDITIONALLY — even for an empty payload {} or a script that never
181
+ // reads stdin — so the child can never block waiting for input and hit
182
+ // the runtime hook timeout (reported as "Hook X timed out after 30000ms").
183
+ child.stdin.write(JSON.stringify(payload))
184
+ child.stdin.end()
185
+ })
186
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * pantheon-hooks.ts — Pantheon runtime security-hook plugin for OpenCode.
3
+ *
4
+ * Re-created per Council Synthesis 2026-08-05 (P0): the previous hook plugin
5
+ * was removed on 24-25/07 because it used the Bun Shell `$` from the plugin
6
+ * API, whose interface has NO `.timeout()` method — every hook crashed with
7
+ * `TypeError: $.quiet().nothrow().timeout is not a function`. The 10 functional
8
+ * scripts in scripts/hooks/ (gitleaks covers only staged files, NOT tool
9
+ * calls) were left orphaned. This plugin wires them back into the opencode
10
+ * runtime via `node:child_process` (version-proof — see hook-runner.ts).
11
+ *
12
+ * Hook mapping (Claude Code protocol payload {tool_name, tool_input,
13
+ * agent_id, session_id} delivered on the script stdin):
14
+ * - tool.execute.before → validate-talos-scope.sh, scan-secrets.sh,
15
+ * validate-tool-safety.sh + on-subagent-delegation-start.sh (only when the
16
+ * tool is a delegation tool such as `task`, to keep the audit log honest)
17
+ * - tool.execute.after → format-multi-language.sh, log-session-start.sh +
18
+ * on-subagent-delegation-stop.sh (delegation tool only)
19
+ * - event session.created → log-session-start.sh, validate-post-conditions.sh
20
+ *
21
+ * Notes:
22
+ * - `agent_id` is NOT present in the opencode tool.execute hook payloads
23
+ * ({tool, sessionID, callID}), so it is passed as '' — validate-talos-scope
24
+ * then takes its safe skip path; the agent-agnostic scan-secrets and
25
+ * validate-tool-safety protections remain fully active.
26
+ * - tool.execute hooks cannot hard-deny the tool call through this API
27
+ * (the output only mutates args; real deny is the permission system), so
28
+ * violations surface as stderr + non-zero exit codes logged below.
29
+ * - Logging policy: non-zero exit → console.error (the security signal);
30
+ * zero exit → SILENT by default. Audit scripts (log-session-start,
31
+ * on-subagent-delegation-*) still write their log FILES (sessions.log,
32
+ * delegations.log) from inside the .sh scripts — only the console echo is
33
+ * suppressed so the chat/TUI stays clean. Set PANTHEON_HOOKS_LOG=1 (or
34
+ * "debug") to re-enable the zero-exit console echo for debugging.
35
+ * The env var is read once at plugin load time (opencode startup).
36
+ * - Hooks NEVER throw: every body is try/catch'd and only logs.
37
+ *
38
+ * IMPORTANT (OpenCode 1.18.11 legacy loader): this module must export EXACTLY
39
+ * ONE function-valued export — the default plugin (see src/plugin.ts L47-52).
40
+ * Helpers live in ./hook-runner.ts and are imported from there.
41
+ */
42
+ import type { Plugin } from '@opencode-ai/plugin'
43
+ import { runHook, type HookPayload } from './hook-runner.ts'
44
+
45
+ /** Tools that represent a subagent delegation (opencode `task` tool etc.). */
46
+ const DELEGATION_TOOL_RE = /^(task|.*delegate.*|.*subagent.*)$/i
47
+
48
+ /**
49
+ * When set to a truthy value ("1" or "debug"), zero-exit audit-hook output is
50
+ * echoed to the console (opt-in debugging). Default (unset): fully silent on
51
+ * success — the .sh scripts still persist their log FILES on disk; only the
52
+ * console echo is suppressed. Read at plugin load time.
53
+ */
54
+ const AUDIT_LOG_ENABLED = (process.env.PANTHEON_HOOKS_LOG ?? '').trim() !== ''
55
+
56
+ /** Scripts whose zero-exit stderr is relayed when PANTHEON_HOOKS_LOG is set. */
57
+ const AUDIT_HOOKS = new Set([
58
+ 'log-session-start.sh',
59
+ 'on-subagent-delegation-start.sh',
60
+ 'on-subagent-delegation-stop.sh',
61
+ 'validate-post-conditions.sh',
62
+ ])
63
+
64
+ function trim(s: string, max = 2000): string {
65
+ const t = s.trim()
66
+ return t.length > max ? `${t.slice(0, max)}…` : t
67
+ }
68
+
69
+ /** Translate the opencode hook event into the Claude Code protocol payload. */
70
+ function payloadFor(
71
+ input: { tool: string; sessionID: string },
72
+ toolInput: unknown,
73
+ ): HookPayload {
74
+ return {
75
+ tool_name: input.tool,
76
+ tool_input: toolInput ?? {},
77
+ // tool.execute hooks carry no agent — '' lets talos-scope skip safely.
78
+ agent_id: (input as { agent?: { id?: string } }).agent?.id ?? '',
79
+ session_id: input.sessionID,
80
+ }
81
+ }
82
+
83
+ /** Run one hook with the logging policy; NEVER throw out of the hook. */
84
+ async function safeRun(script: string, payload: HookPayload): Promise<void> {
85
+ try {
86
+ const result = await runHook(script, payload)
87
+ const tag = `[pantheon-hooks:${script}]`
88
+ if (result.code !== 0) {
89
+ console.error(
90
+ `${tag} exit ${result.code}${result.timedOut ? ' (timed out)' : ''}: ${trim(result.stderr || result.stdout || 'no output')}`,
91
+ )
92
+ } else if (AUDIT_LOG_ENABLED && AUDIT_HOOKS.has(script) && result.stderr.trim()) {
93
+ // Opt-in only (PANTHEON_HOOKS_LOG=1): audit scripts echo their FILE
94
+ // writes here for debugging. Default is silence — the .sh scripts
95
+ // already persist sessions.log / delegations.log on disk.
96
+ console.log(`${tag} ${trim(result.stderr)}`)
97
+ }
98
+ } catch (err) {
99
+ // runHook never throws, but the hook body must survive anything.
100
+ console.error(
101
+ `[pantheon-hooks:${script}] unexpected failure: ${err instanceof Error ? err.message : String(err)}`,
102
+ )
103
+ }
104
+ }
105
+
106
+ const plugin: Plugin = async () => {
107
+ return {
108
+ 'tool.execute.before': async (input, output) => {
109
+ try {
110
+ const payload = payloadFor(input, output.args ?? {})
111
+ const hooks = [
112
+ 'validate-talos-scope.sh',
113
+ 'scan-secrets.sh',
114
+ 'validate-tool-safety.sh',
115
+ ]
116
+ if (DELEGATION_TOOL_RE.test(input.tool)) {
117
+ hooks.push('on-subagent-delegation-start.sh')
118
+ }
119
+ await Promise.all(hooks.map((script) => safeRun(script, payload)))
120
+ } catch {
121
+ // Never let a hook take down the opencode session.
122
+ }
123
+ },
124
+ 'tool.execute.after': async (input) => {
125
+ try {
126
+ const payload = payloadFor(input, input.args ?? {})
127
+ const hooks = ['format-multi-language.sh']
128
+ if (DELEGATION_TOOL_RE.test(input.tool)) {
129
+ hooks.push('on-subagent-delegation-stop.sh')
130
+ }
131
+ await Promise.all(hooks.map((script) => safeRun(script, payload)))
132
+ } catch {
133
+ // Never let a hook take down the opencode session.
134
+ }
135
+ },
136
+ event: async ({ event: ev }) => {
137
+ try {
138
+ if (ev.type === 'session.created') {
139
+ const payload: HookPayload = { session_id: ev.properties.info.id }
140
+ await Promise.all(
141
+ ['log-session-start.sh', 'validate-post-conditions.sh'].map((script) =>
142
+ safeRun(script, payload),
143
+ ),
144
+ )
145
+ }
146
+ } catch {
147
+ // Never let a hook take down the opencode session.
148
+ }
149
+ },
150
+ }
151
+ }
152
+
153
+ export default plugin
@@ -5,4 +5,4 @@ declare const plugin: TuiPluginModule & {
5
5
  };
6
6
  //#endregion
7
7
  export { plugin as default };
8
- //# sourceMappingURL=tui.d.ts.map
8
+ //# sourceMappingURL=tui.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;cAsbM,QAAQ;EAAoB"}
1
+ {"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;cA8qCM,QAAQ;EAAoB"}