pantheon-opencode 1.4.3 → 1.5.0-beta.10

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 (118) hide show
  1. package/.pantheon/code-mode/README.md +93 -0
  2. package/.pantheon/code-mode/checkpoint-session.py +7 -0
  3. package/.pantheon/code-mode/checkpoint-session.sh +11 -0
  4. package/.pantheon/code-mode/checkpoint_session.py +481 -0
  5. package/.pantheon/code-mode/compress-inline.py +450 -0
  6. package/.pantheon/code-mode/eval-llm-judge.py +255 -0
  7. package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
  8. package/.pantheon/code-mode/eval-run.py +197 -0
  9. package/.pantheon/code-mode/eval-static.py +321 -0
  10. package/.pantheon/code-mode/example-sync.sh +5 -0
  11. package/.pantheon/code-mode/manifest.json +21 -0
  12. package/.pantheon/code-mode/session-end-save.py +186 -0
  13. package/.pantheon/code-mode/session-end-save.sh +18 -0
  14. package/.pantheon/code-mode/test-allowed.sh +5 -0
  15. package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
  16. package/.pantheon/code-mode/test-env.sh +7 -0
  17. package/.pantheon/code-mode/test-prlimit.sh +9 -0
  18. package/.pantheon/code-mode/test-timeout.sh +6 -0
  19. package/CHANGELOG.md +1026 -0
  20. package/README.md +292 -1080
  21. package/README.pt-BR.md +308 -0
  22. package/bin/pantheon-init.mjs +207 -31
  23. package/opencode.json +1 -0
  24. package/package.json +34 -14
  25. package/plugin.json +15 -0
  26. package/pyproject.toml +65 -0
  27. package/scripts/_pantheon_paths.py +48 -7
  28. package/scripts/ci-validate-yaml.py +7 -2
  29. package/scripts/code_mode_server.py +451 -44
  30. package/scripts/cost.mjs +30 -14
  31. package/scripts/doctor.mjs +306 -129
  32. package/scripts/generate-preset-docs.mjs +127 -66
  33. package/scripts/generate-prompts.sh +2 -2
  34. package/scripts/install/cli-ui.mjs +23 -10
  35. package/scripts/install/config-migration.mjs +399 -0
  36. package/scripts/install/health-check.mjs +3 -3
  37. package/scripts/install/model-picker.mjs +72 -23
  38. package/scripts/install/opencode-version.mjs +47 -0
  39. package/scripts/install/opencode.mjs +396 -121
  40. package/scripts/install/plugin.mjs +89 -49
  41. package/scripts/install/shared.mjs +55 -159
  42. package/scripts/install/strings.mjs +129 -0
  43. package/scripts/install-mcp.mjs +45 -232
  44. package/scripts/manifest-inventory.mjs +410 -0
  45. package/scripts/mcp_persistence_server.py +798 -60
  46. package/scripts/mcp_resources_server.py +144 -40
  47. package/scripts/memory_mcp_server.py +2 -2
  48. package/scripts/package-evidence.mjs +242 -0
  49. package/scripts/probe-context-rehydrate.mjs +240 -0
  50. package/scripts/probe-pantheon-cost.mjs +313 -0
  51. package/scripts/recover-zenodo-deposition.mjs +146 -0
  52. package/scripts/release-beta-version.mjs +56 -60
  53. package/scripts/sync-artifacts.mjs +169 -0
  54. package/scripts/sync-tui.mjs +135 -42
  55. package/scripts/test-opencode-v1-v2-sandbox.sh +862 -0
  56. package/scripts/uninstall.mjs +298 -954
  57. package/scripts/validate-zenodo-release.mjs +154 -0
  58. package/scripts/validation-policy.mjs +7 -7
  59. package/scripts/version-check.mjs +113 -108
  60. package/scripts/versioning.mjs +230 -86
  61. package/scripts/zenodo-artifact.mjs +21 -0
  62. package/scripts/zenodo-release-state.mjs +53 -0
  63. package/src/agents/mnemosyne.md +1 -1
  64. package/src/agents/zeus.md +27 -16
  65. package/src/mcp/_pantheon_paths.py +48 -7
  66. package/src/mcp/code_mode_server.py +451 -44
  67. package/src/mcp/eval_store.py +1 -1
  68. package/src/mcp/mcp_persistence_server.py +798 -60
  69. package/src/mcp/mcp_resources_server.py +144 -40
  70. package/src/mcp/pantheon_vision_server.py +1 -1
  71. package/src/mcp/toon_codec.py +491 -0
  72. package/src/pantheon/auto-wake.ts +1 -5
  73. package/src/pantheon/background-job-board.ts +110 -2
  74. package/src/pantheon/compaction-handoff.ts +304 -0
  75. package/src/pantheon/cost-command.ts +356 -69
  76. package/src/pantheon/delegate-manager.ts +766 -0
  77. package/src/pantheon/delegation-classifier.ts +307 -0
  78. package/src/pantheon/delegation-enforce.ts +13 -12
  79. package/src/pantheon/delegation-finalize.ts +406 -0
  80. package/src/pantheon/delegation-notify.ts +212 -0
  81. package/src/pantheon/delegation-toast.ts +73 -0
  82. package/src/pantheon/delegation.ts +1337 -0
  83. package/src/pantheon/file-persistence-salvage.ts +64 -0
  84. package/src/pantheon/file-persistence.ts +194 -45
  85. package/src/pantheon/goal-loop.ts +2 -2
  86. package/src/pantheon/goal-store.ts +1 -1
  87. package/src/pantheon/hashline/tool.ts +2 -2
  88. package/src/pantheon/model-command.ts +2 -1
  89. package/src/pantheon/native-probe.ts +234 -0
  90. package/src/pantheon/native-task-status.ts +139 -0
  91. package/src/pantheon/permission-eval.ts +284 -0
  92. package/src/pantheon/provider-capabilities.ts +221 -0
  93. package/src/pantheon/provider-stream-tracker.ts +182 -0
  94. package/src/pantheon/shared-board.ts +37 -0
  95. package/src/pantheon/step-cap.ts +110 -0
  96. package/src/pantheon/task-result-guard.ts +107 -0
  97. package/src/pantheon/token-opt.ts +689 -0
  98. package/src/pantheon/tool-ceiling.ts +159 -0
  99. package/src/pantheon/toon-codec.ts +481 -0
  100. package/src/pantheon/tui-version-check.ts +1 -3
  101. package/src/pantheon/v2-bridge.ts +129 -0
  102. package/src/pantheon/v2-events.ts +148 -0
  103. package/src/pantheon/v2-hooks.ts +287 -0
  104. package/src/pantheon/v2-tool-definitions.ts +331 -0
  105. package/src/pantheon/zeus-delegate-with-retry.ts +406 -0
  106. package/src/plugin-v2/index.ts +21 -0
  107. package/src/plugin-v2.ts +824 -0
  108. package/src/plugin.ts +261 -51
  109. package/src/plugins/pantheon-hooks.ts +51 -0
  110. package/src/plugins/tui/dist/tui.d.ts +116 -24
  111. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  112. package/src/plugins/tui/dist/tui.js +416 -149
  113. package/src/plugins/tui/dist/tui.tsx +456 -87
  114. package/src/plugins/tui/package-lock.json +51 -51
  115. package/src/plugins/tui/package.json +7 -7
  116. package/src/plugins/tui/src/index.tsx +462 -95
  117. package/src/skills/README.md +0 -2
  118. package/src/pantheon/tool-context.ts +0 -41
@@ -0,0 +1,129 @@
1
+ /**
2
+ * V1→V2 Bridge — passes V1 infrastructure singletons through V2 context options.
3
+ *
4
+ * The V2 plugin (`plugin-v2.ts`) registers tools via `ctx.tool.transform()` but
5
+ * lacks the runtime singletons (BackgroundJobBoard, delegation client, goal store,
6
+ * todo enforcer, vision handler) that live in the V1 plugin. This bridge:
7
+ *
8
+ * 1. Wraps V1 singletons into a portable `PantheonV2Bridge` interface.
9
+ * 2. Attaches the bridge to V2 `ctx.options` so V2 tool handlers can access it.
10
+ * 3. Provides `getV2BridgeFromContext()` for V2 tools to retrieve the bridge.
11
+ *
12
+ * Design: the bridge is **optional** — V2 plugin works without it (graceful
13
+ * fallback with limited functionality). V1 and V2 are never directly coupled;
14
+ * communication is via the `PantheonV2Bridge` interface only.
15
+ *
16
+ * @module v2-bridge
17
+ */
18
+
19
+ import type { BackgroundJobBoard } from './background-job-board.ts'
20
+ import type { DelegationClient } from './delegation-finalize.ts'
21
+ import type { GoalStore } from './goal-store.ts'
22
+ import type { TodoEnforcer } from './todo-enforcer.ts'
23
+
24
+ // ─── Types ─────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Minimal structural view of the V2 PluginContext — we only need `options`
28
+ * to inject/retrieve the bridge. Avoids importing V2 SDK types directly.
29
+ */
30
+ export interface V2ContextLike {
31
+ options: Record<string, unknown>
32
+ }
33
+
34
+ /** Structural view of the V1 PluginInput — the fields the bridge needs. */
35
+ export interface V1PluginInput {
36
+ client: unknown
37
+ directory: string
38
+ }
39
+
40
+ /**
41
+ * Vision handler surface — the subset of the V1 vision handler the bridge
42
+ * exposes. Full type is the return of `createVisionHandler(input)`, but we
43
+ * only need the hook functions for V2 event delegation.
44
+ */
45
+ export interface VisionHandler {
46
+ chatMessage: (...args: unknown[]) => Promise<void>
47
+ messagesTransform: (...args: unknown[]) => Promise<void>
48
+ event: (...args: unknown[]) => Promise<void>
49
+ }
50
+
51
+ /**
52
+ * Full bridge interface — all V1 infrastructure V2 tools need.
53
+ *
54
+ * Every field is optional to support partial initialization and graceful
55
+ * degradation when V1 infrastructure is not available.
56
+ */
57
+ export interface PantheonV2Bridge {
58
+ /** Background job board — tracks running/completed/cancelled delegated jobs. */
59
+ board?: BackgroundJobBoard
60
+ /** Structural delegation client — creates child sessions and sends prompts. */
61
+ delegationClient?: DelegationClient
62
+ /** Goal store — file-based persistence for full-auto goal loop. */
63
+ goalStore?: GoalStore
64
+ /** Todo enforcer — idle continuation when todos are incomplete. */
65
+ todoEnforcer?: TodoEnforcer
66
+ /** Vision handler — image analysis, chat message enrichment, event handling. */
67
+ visionHandler?: VisionHandler
68
+ }
69
+
70
+ /**
71
+ * Extended V2 context options that carry the bridge.
72
+ * The bridge is attached under the `__pantheonV1Bridge` key to avoid
73
+ * collisions with user-defined options.
74
+ */
75
+ export const BRIDGE_OPTIONS_KEY = '__pantheonV1Bridge' as const
76
+
77
+ // ─── Bridge Factory ────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Create a bridge from V1 singletons.
81
+ *
82
+ * Call this inside the V1 plugin's `setup()` AFTER the singletons are
83
+ * initialized, then pass the result to V2 via `ctx.options`.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * // In V1 plugin setup (plugin.ts):
88
+ * const bridge = createV2Bridge({ board, delegationClient, goalStore, ... })
89
+ * // Then pass bridge to V2 context via options
90
+ * ```
91
+ */
92
+ export function createV2Bridge(singletons: PantheonV2Bridge): PantheonV2Bridge {
93
+ // Return a frozen copy — bridge consumers must not mutate singletons.
94
+ return Object.freeze({ ...singletons })
95
+ }
96
+
97
+ /**
98
+ * Retrieve the bridge from a V2 PluginContext.
99
+ *
100
+ * Returns `null` when the bridge was not injected (V2 running standalone
101
+ * or V1 not yet initialized). V2 tools should check for null and
102
+ * degrade gracefully.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * // In a V2 tool handler:
107
+ * const bridge = getV2BridgeFromContext(ctx)
108
+ * if (!bridge?.board) {
109
+ * return { text: 'Background job board unavailable — delegation disabled.' }
110
+ * }
111
+ * ```
112
+ */
113
+ export function getV2BridgeFromContext(ctx: V2ContextLike): PantheonV2Bridge | null {
114
+ const bridge = ctx.options[BRIDGE_OPTIONS_KEY]
115
+ if (bridge != null && typeof bridge === 'object') {
116
+ return bridge as PantheonV2Bridge
117
+ }
118
+ return null
119
+ }
120
+
121
+ /**
122
+ * Inject the bridge into a V2 PluginContext's options.
123
+ *
124
+ * Call this in the V1 plugin to make the bridge available to V2 tools.
125
+ * Safe to call multiple times — subsequent calls overwrite the previous bridge.
126
+ */
127
+ export function injectBridge(ctx: V2ContextLike, bridge: PantheonV2Bridge): void {
128
+ ctx.options[BRIDGE_OPTIONS_KEY] = bridge
129
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * V2 Event Handlers — Pantheon event subscriptions for the V2 plugin API.
3
+ *
4
+ * Subscribes to session lifecycle events (created, idle, error, compacted)
5
+ * and routes them to the appropriate subsystem:
6
+ * - session.created → seed session hierarchy + root sessions
7
+ * - session.idle → delegation finalize / goal loop / todo enforcer
8
+ * - session.error → delegation finalize as error
9
+ * - session.compacted → todo preserve / compaction context
10
+ *
11
+ * These handlers are PURE — they depend only on structural interfaces,
12
+ * not on the V1 PluginInput or V2 PluginContext.
13
+ *
14
+ * @module pantheon/v2-events
15
+ */
16
+
17
+ import type { BackgroundJobBoard } from './background-job-board.ts'
18
+ import type { FinalizeInput } from './delegation-finalize.ts'
19
+ import type { DelegationEventDeps, DelegationEventLike } from './delegation-notify.ts'
20
+ import { handleDelegationEvent } from './delegation-notify.ts'
21
+ import type { GoalLoopLike, TodoEnforcerLike } from './idle-continuation.ts'
22
+ import { createIdleDispatcher, type IdleDispatcher } from './idle-continuation.ts'
23
+ import { createPantheonLogger } from './logger.ts'
24
+
25
+ const log = createPantheonLogger({ module: 'pantheon-v2-events' })
26
+
27
+ // ─── Types ───────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Structural session info from session.created events.
31
+ */
32
+ export interface SessionInfo {
33
+ id: string
34
+ parentID?: string
35
+ }
36
+
37
+ /**
38
+ * Structural event from the V2 event stream.
39
+ */
40
+ export interface V2SessionEvent {
41
+ type: string
42
+ properties?: Record<string, unknown>
43
+ }
44
+
45
+ /**
46
+ * Dependencies for the V2 event dispatcher.
47
+ */
48
+ export interface V2EventDeps {
49
+ board: BackgroundJobBoard
50
+ finalize: (childSessionID: string, opts: FinalizeInput) => Promise<unknown>
51
+ goalLoop: GoalLoopLike
52
+ todoEnforcer: TodoEnforcerLike
53
+ /** Callback to register a session in the hierarchy. */
54
+ registerSession?: (info: SessionInfo) => void
55
+ /** Callback to add a session as root. */
56
+ addRootSession?: (sessionID: string) => void
57
+ }
58
+
59
+ /**
60
+ * V2 Event Dispatcher — routes session events to subsystems.
61
+ */
62
+ export interface V2EventDispatcher {
63
+ /** Process a session event. Returns true if handled. */
64
+ handleEvent(event: V2SessionEvent): Promise<boolean>
65
+ /** Cleanup subscriptions. */
66
+ dispose(): void
67
+ }
68
+
69
+ // ─── Implementation ──────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Create the V2 event dispatcher. Routes session lifecycle events to
73
+ * delegation finalize, goal loop, and todo enforcer.
74
+ *
75
+ * Usage:
76
+ * ```ts
77
+ * const dispatcher = createV2EventDispatcher(deps)
78
+ * // In event subscription loop:
79
+ * for await (const event of ctx.event.subscribe(...)) {
80
+ * await dispatcher.handleEvent(event)
81
+ * }
82
+ * ```
83
+ */
84
+ export function createV2EventDispatcher(deps: V2EventDeps): V2EventDispatcher {
85
+ const delegationDeps: DelegationEventDeps = {
86
+ board: deps.board,
87
+ finalize: deps.finalize,
88
+ }
89
+ const idleDispatcher: IdleDispatcher = createIdleDispatcher({
90
+ goalLoop: deps.goalLoop,
91
+ todoEnforcer: deps.todoEnforcer,
92
+ })
93
+
94
+ return {
95
+ handleEvent: async (event: V2SessionEvent): Promise<boolean> => {
96
+ try {
97
+ // session.created → seed hierarchy + board put-if-absent for native children
98
+ if (event.type === 'session.created') {
99
+ const info = event.properties?.info as SessionInfo | undefined
100
+ if (info) {
101
+ deps.registerSession?.(info)
102
+ if (info.parentID === undefined) {
103
+ deps.addRootSession?.(info.id)
104
+ } else {
105
+ // Native task(background=true) child: register on the board
106
+ // atomically (skip if V1 idle hook or re-dispatch already claimed it).
107
+ try {
108
+ await deps.board.registerLaunchIfAbsent({
109
+ taskID: info.id,
110
+ parentSessionID: info.parentID,
111
+ agent: 'native',
112
+ description: `native child ${info.id}`,
113
+ })
114
+ } catch (err) {
115
+ // The per-agent concurrency limit can refuse the launch. Do not
116
+ // abort the rest of the handler: this child is simply not
117
+ // mirrored on the board, while finalize/idle dispatch for every
118
+ // other event must still run.
119
+ log.warn(`[Pantheon V2] native child ${info.id} not mirrored on board:`, err)
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ // Delegate to handleDelegationEvent (covers idle + error for board children)
126
+ const delegated = await handleDelegationEvent(event as DelegationEventLike, delegationDeps)
127
+
128
+ // Non-board idle → idle dispatcher (goal loop / todo enforcer)
129
+ if (!delegated && event.type === 'session.idle') {
130
+ const sessionID = event.properties?.sessionID as string | undefined
131
+ if (sessionID) {
132
+ await idleDispatcher.onIdle(sessionID)
133
+ }
134
+ }
135
+
136
+ return delegated
137
+ } catch (err) {
138
+ log.error('[Pantheon V2] Event handling failed:', err)
139
+ return false
140
+ }
141
+ },
142
+
143
+ dispose: () => {
144
+ // No persistent subscriptions to clean up — the event stream
145
+ // lifecycle is managed by the caller.
146
+ },
147
+ }
148
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * V2 Session Hooks — Pantheon session hooks for the V2 plugin API.
3
+ *
4
+ * Provides two hook types:
5
+ * - `"prompt"` hook: Vision message interception (paste-and-ask). Intercepts
6
+ * user messages with pasted images and replaces them with text instructions
7
+ * or native vision descriptions.
8
+ * - `"context"` hook: Inject routing policy, active goals, and pending todos
9
+ * into the system context before each LLM call.
10
+ *
11
+ * These handlers are PURE — they depend only on structural interfaces.
12
+ *
13
+ * @module pantheon/v2-hooks
14
+ */
15
+
16
+ import type { BackgroundJobBoard } from './background-job-board.ts'
17
+ import { buildCompactionContext } from './delegation-compaction.ts'
18
+ import type { GoalStore } from './goal-loop.ts'
19
+ import { createPantheonLogger } from './logger.ts'
20
+ import type { TodoEnforcer } from './todo-enforcer.ts'
21
+
22
+ const log = createPantheonLogger({ module: 'pantheon-v2-hooks' })
23
+
24
+ // ─── Types ───────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Structural context event passed to the "context" hook.
28
+ * V2 API: `ctx.session.hook("context", (event) => { ... })`
29
+ */
30
+ export interface V2ContextHookEvent {
31
+ sessionID: string
32
+ // Unknown: host beta may send string, string[], SystemPart objects
33
+ // ({type,text}), mixed arrays, or non-array. Narrow internally (fail-open).
34
+ system: unknown
35
+ generation: {
36
+ temperature?: number
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Structural prompt event passed to the "prompt" hook.
42
+ * V2 API: `ctx.session.hook("prompt", (event) => { ... })`
43
+ */
44
+ export interface V2PromptHookEvent {
45
+ sessionID: string
46
+ message: unknown
47
+ }
48
+
49
+ /**
50
+ * Dependencies for session hooks.
51
+ */
52
+ export interface V2HookDeps {
53
+ board: BackgroundJobBoard
54
+ goalStore: GoalStore
55
+ todoEnforcer: TodoEnforcer
56
+ }
57
+
58
+ // ─── Context Hook ────────────────────────────────────────────────────────
59
+
60
+ const POLICY_MARKER = '<!-- pantheon-v2-policy -->'
61
+ const ROUTING_POLICY =
62
+ `${POLICY_MARKER}\nFollow Pantheon routing policy: delegate implementation work ` +
63
+ `to the named specialist and do not claim work was performed without verification.`
64
+
65
+ /** Default max compaction items (matching routing.yml background_delegation.max_compaction_items). */
66
+ const DEFAULT_MAX_COMPACTION_ITEMS = 10
67
+
68
+ /**
69
+ * Extract comparable text from a system entry: plain string or
70
+ * SystemPart-like object ({type,text}). Returns null for junk (null/number).
71
+ * Runtime sniffing: pinned SDK (@opencode-ai/plugin 1.18.18) only knows
72
+ * string/string[] — host beta sends objects. Never throws (fail-open).
73
+ */
74
+ function systemEntryText(entry: unknown): string | null {
75
+ try {
76
+ if (typeof entry === 'string') return entry
77
+ if (
78
+ typeof entry === 'object' &&
79
+ entry !== null &&
80
+ typeof (entry as { text?: unknown }).text === 'string'
81
+ ) {
82
+ return (entry as { text: string }).text
83
+ }
84
+ return null
85
+ } catch {
86
+ return null
87
+ }
88
+ }
89
+
90
+ function hasPolicyMarker(arr: unknown[]): boolean {
91
+ try {
92
+ return arr.some((s) => {
93
+ const t = systemEntryText(s)
94
+ return t?.includes(POLICY_MARKER) ?? false
95
+ })
96
+ } catch {
97
+ return false
98
+ }
99
+ }
100
+
101
+ function usesObjectShape(arr: unknown[]): boolean {
102
+ try {
103
+ return arr.some(
104
+ (s) =>
105
+ typeof s === 'object' && s !== null && typeof (s as { text?: unknown }).text === 'string',
106
+ )
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ function toSystemEntry(text: string, useObject: boolean): string | { type: 'text'; text: string } {
113
+ return useObject ? { type: 'text', text } : text
114
+ }
115
+
116
+ /**
117
+ * Context hook handler — injects Pantheon policy, routing, and compaction
118
+ * state into the system context before each LLM call.
119
+ *
120
+ * Registered via: `ctx.session.hook("context", handler)`
121
+ */
122
+ export function createV2ContextHookHandler(deps: V2HookDeps) {
123
+ return async (event: V2ContextHookEvent): Promise<void> => {
124
+ try {
125
+ // Beta 19192 hardening (defense-in-depth) + SystemPart sniffing: the
126
+ // host may send string[], SystemPart objects ({type,text}), mixed
127
+ // arrays, a single string, or a non-array system. Preserve the string
128
+ // path behavior; inject object shape when the array contains objects
129
+ // (else host schema validation fails on system[N]). Never throw.
130
+ const rawSystem: unknown = (event as { system?: unknown } | null | undefined)?.system
131
+ let system: unknown[]
132
+ if (typeof rawSystem === 'string') {
133
+ system = rawSystem.includes(POLICY_MARKER) ? [rawSystem] : [rawSystem, ROUTING_POLICY]
134
+ ;(event as { system?: unknown }).system = system
135
+ } else if (Array.isArray(rawSystem)) {
136
+ // 1. Inject routing policy if not already present (string OU .text)
137
+ if (!hasPolicyMarker(rawSystem)) {
138
+ rawSystem.push(toSystemEntry(ROUTING_POLICY, usesObjectShape(rawSystem)))
139
+ }
140
+ system = rawSystem
141
+ } else {
142
+ // Non-string, non-array system: ignore silently (fail-open).
143
+ return
144
+ }
145
+
146
+ // 2. Inject compaction context (active goals, pending todos, delegations)
147
+ // Convert blocks to the same shape as the host array (object vs string).
148
+ const blocks = await buildCompactionContext(deps.board, {
149
+ sessionID: event.sessionID,
150
+ maxItems: DEFAULT_MAX_COMPACTION_ITEMS,
151
+ goals: {
152
+ enabled: true,
153
+ list: (sessionID: string) => deps.goalStore.list(sessionID),
154
+ },
155
+ todos: {
156
+ enabled: true,
157
+ list: (sessionID: string) => deps.todoEnforcer.listPendingTodos(sessionID),
158
+ },
159
+ })
160
+ if (blocks.length > 0) {
161
+ const useObject = usesObjectShape(system)
162
+ for (const b of blocks) {
163
+ system.push(useObject ? { type: 'text' as const, text: b } : b)
164
+ }
165
+ }
166
+
167
+ // 3. Apply conservative temperature for routing decisions
168
+ const generation = (event as { generation?: { temperature?: number } }).generation
169
+ if (generation != null && generation.temperature === undefined) {
170
+ generation.temperature = 0.2
171
+ }
172
+ } catch (err) {
173
+ log.warn('[Pantheon V2] Context hook failed:', err)
174
+ // Fail-open: never break the session
175
+ }
176
+ }
177
+ }
178
+
179
+ // ─── Prompt Hook ─────────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Prompt hook handler — intercepts user messages for vision processing.
183
+ *
184
+ * Registered via: `ctx.session.hook("prompt", handler)`
185
+ *
186
+ * Note: The full vision handler is complex (file I/O, provider API calls).
187
+ * This is a minimal adapter that delegates to the V1 vision handler
188
+ * if available. Without vision infrastructure, it's a no-op.
189
+ */
190
+ export function createV2PromptHookHandler(_deps: V2HookDeps) {
191
+ return async (event: V2PromptHookEvent): Promise<void> => {
192
+ // Vision interception is a complex subsystem (see src/pantheon/vision.ts).
193
+ // The V2 prompt hook delegates to the V1 vision handler via the plugin's
194
+ // integration point. Without V1 client access, this is a graceful no-op.
195
+ //
196
+ // TODO: Wire full vision handler when V2 provides SDK client access.
197
+ void event
198
+ }
199
+ }
200
+
201
+ // ─── Tool Hooks ──────────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Tool execute.before hook — enforces read-only restrictions, command
205
+ * normalization, and todo preservation.
206
+ *
207
+ * Registered via: `ctx.tool.hook("execute.before", handler)`
208
+ */
209
+ export interface V2ToolBeforeEvent {
210
+ tool: string
211
+ sessionID: string
212
+ callID: string
213
+ args?: unknown
214
+ }
215
+
216
+ export function createV2ToolBeforeHookHandler(
217
+ enforcementGuard: (
218
+ input: { tool: string; sessionID: string; callID: string },
219
+ output?: { args?: unknown },
220
+ ) => void,
221
+ commandNormalizer: (
222
+ input: { tool: string; sessionID: string; callID: string },
223
+ output?: { args?: unknown },
224
+ ) => Promise<void>,
225
+ ) {
226
+ return async (event: V2ToolBeforeEvent): Promise<void> => {
227
+ try {
228
+ enforcementGuard(
229
+ { tool: event.tool, sessionID: event.sessionID, callID: event.callID },
230
+ { args: event.args },
231
+ )
232
+ await commandNormalizer(
233
+ { tool: event.tool, sessionID: event.sessionID, callID: event.callID },
234
+ { args: event.args },
235
+ )
236
+ } catch (err) {
237
+ log.warn('[Pantheon V2] Tool before-hook failed:', err)
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Tool execute.after hook — augments read output with hashline tags
244
+ * and applies context sandbox truncation.
245
+ *
246
+ * Registered via: `ctx.tool.hook("execute.after", handler)`
247
+ */
248
+ export interface V2ToolAfterEvent {
249
+ tool: string
250
+ sessionID: string
251
+ callID: string
252
+ args?: unknown
253
+ // The host may send a non-string output (shape drift on beta). Keep this
254
+ // honest as unknown and narrow with typeof at the use site — never assume.
255
+ output: unknown
256
+ }
257
+
258
+ export function createV2ToolAfterHookHandler(
259
+ readEnhancer: (
260
+ input: { tool: string; sessionID: string; callID: string; args?: unknown },
261
+ output: { output: string },
262
+ ) => Promise<void>,
263
+ sandboxHandler: (
264
+ input: { tool: string; sessionID: string; callID: string; args?: unknown },
265
+ output: { output: string },
266
+ ) => Promise<void>,
267
+ ) {
268
+ return async (event: V2ToolAfterEvent): Promise<void> => {
269
+ try {
270
+ const input = {
271
+ tool: event.tool,
272
+ sessionID: event.sessionID,
273
+ callID: event.callID,
274
+ args: event.args,
275
+ }
276
+ // Narrow the honest unknown: only strings flow through the enhancers.
277
+ // Non-string output is left untouched (fail-open, preserve host shape).
278
+ if (typeof event.output !== 'string') return
279
+ const output = { output: event.output }
280
+ await sandboxHandler(input, output)
281
+ await readEnhancer(input, output)
282
+ event.output = output.output
283
+ } catch (err) {
284
+ log.warn('[Pantheon V2] Tool after-hook failed:', err)
285
+ }
286
+ }
287
+ }