pantheon-opencode 1.5.0-beta.2 → 1.5.0-beta.20
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/manifest.json +21 -0
- package/AGENTS.md +14 -63
- package/CHANGELOG.md +1260 -0
- package/README.md +162 -23
- package/README.pt-BR.md +104 -19
- package/bin/pantheon-init.mjs +178 -21
- package/commands/pantheon.md +1 -35
- package/package.json +23 -15
- package/plugin.json +1 -1
- package/pyproject.toml +62 -0
- package/scripts/code_mode_server.py +431 -55
- package/scripts/doctor.mjs +207 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +137 -35
- package/scripts/install/plugin.mjs +12 -45
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +536 -164
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-pantheon-cost.mjs +10 -1
- package/scripts/release-beta-version.mjs +56 -60
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +129 -64
- package/scripts/test-opencode-v1-v2-sandbox.sh +79 -161
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +230 -86
- package/src/agents/apollo.md +1 -0
- package/src/agents/gaia.md +1 -0
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +11 -54
- package/src/instructions/memory-protocol.instructions.md +6 -3
- package/src/instructions/zeus-anti-stall.instructions.md +2 -4
- package/src/instructions/zeus-council-synthesis.instructions.md +3 -28
- package/src/instructions/zeus-timeout-retry.instructions.md +3 -28
- package/src/mcp/code_mode_server.py +431 -55
- package/src/mcp/mcp_persistence_server.py +536 -164
- package/src/mcp/toon_codec.py +2 -4
- package/src/pantheon/background-job-board.ts +47 -5
- package/src/pantheon/{delegation-compaction.ts → compaction-context.ts} +10 -100
- package/src/pantheon/cost-command.ts +309 -44
- package/src/pantheon/delegation-enforce.ts +59 -14
- package/src/pantheon/file-persistence-salvage.ts +64 -0
- package/src/pantheon/file-persistence.ts +194 -45
- package/src/pantheon/goal-loop.ts +1 -1
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/idle-continuation.ts +4 -6
- package/src/pantheon/model-command.ts +1 -2
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/presets.d.mts +0 -20
- package/src/pantheon/presets.mjs +0 -64
- package/src/pantheon/session-guard.ts +2 -2
- package/src/pantheon/session-id.ts +30 -0
- package/src/pantheon/shared-board.ts +37 -0
- package/src/pantheon/task-result-guard.ts +106 -0
- package/src/pantheon/todo-enforcer.ts +3 -3
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/tool-context.ts +23 -0
- package/src/pantheon/v2-bridge.ts +1 -4
- package/src/plugin-v2.ts +8 -21
- package/src/plugin.ts +83 -251
- package/src/plugins/pantheon-hooks.ts +51 -0
- 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 +261 -77
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +640 -234
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +2 -2
- package/src/plugins/tui/src/index.tsx +872 -226
- package/src/routing.yml +0 -16
- package/src/skills/context-compression/SKILL.md +1 -1
- package/scripts/generate-preset-docs.mjs +0 -300
- package/scripts/generate-prompts.sh +0 -222
- package/scripts/hash_verify.py +0 -192
- package/scripts/install/component.mjs +0 -24
- package/scripts/install/migrate-config.mjs +0 -302
- package/scripts/test-context-compression.sh +0 -169
- package/src/pantheon/auto-wake.ts +0 -89
- package/src/pantheon/chat-reminders.ts +0 -9
- package/src/pantheon/comment-checker.ts +0 -48
- package/src/pantheon/compaction-assert.ts +0 -160
- package/src/pantheon/cost-tracker.ts +0 -108
- package/src/pantheon/delegate-manager.ts +0 -669
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -374
- package/src/pantheon/delegation-notify.ts +0 -212
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1120
- package/src/pantheon/dispatch-guard.ts +0 -109
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/preemptive-compact.ts +0 -76
- package/src/pantheon/retry-policy.ts +0 -212
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/token-opt.ts +0 -781
- package/src/pantheon/toon-codec.ts +0 -481
- package/src/pantheon/v2-events.ts +0 -131
- package/src/pantheon/v2-hooks.ts +0 -287
- package/src/pantheon/v2-tool-definitions.ts +0 -331
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -406
- package/src/plugins/tui/dist/tui.tsx +0 -2605
package/src/plugin.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
1
|
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
4
2
|
import type { PluginConfig } from 'opencode'
|
|
5
|
-
import { BackgroundJobBoard } from './pantheon/background-job-board.ts'
|
|
6
3
|
import { createCommandNormalizer } from './pantheon/command-normalizer.ts'
|
|
4
|
+
import { buildCompactionContext } from './pantheon/compaction-context.ts'
|
|
7
5
|
import {
|
|
8
6
|
type ContextSandboxConfig,
|
|
9
7
|
createContextSandbox,
|
|
@@ -12,25 +10,13 @@ import {
|
|
|
12
10
|
DEFAULT_LIMITS as SANDBOX_DEFAULT_LIMITS,
|
|
13
11
|
} from './pantheon/context-sandbox.ts'
|
|
14
12
|
import { createCostCommand } from './pantheon/cost-command.ts'
|
|
15
|
-
import { buildPluginNativeDelegation, resolveDelegateMode } from './pantheon/delegate-manager.ts'
|
|
16
|
-
import {
|
|
17
|
-
collectRootSessionIDs,
|
|
18
|
-
createDelegationTools,
|
|
19
|
-
type DelegationClient,
|
|
20
|
-
resolveDelegationBudgets,
|
|
21
|
-
resolveDelegationTimeoutMs,
|
|
22
|
-
startIdleChildScan,
|
|
23
|
-
} from './pantheon/delegation.ts'
|
|
24
|
-
import { buildCompactionContext } from './pantheon/delegation-compaction.ts'
|
|
25
13
|
import {
|
|
26
14
|
createEnforcementGuard,
|
|
27
15
|
readOnlyRegistry,
|
|
28
16
|
SessionHierarchyRegistry,
|
|
17
|
+
syncReadOnlySession,
|
|
29
18
|
zeusReadGuard,
|
|
30
19
|
} from './pantheon/delegation-enforce.ts'
|
|
31
|
-
import { handleDelegationEvent } from './pantheon/delegation-notify.ts'
|
|
32
|
-
import { showDelegationTerminalToast, type ToastClient } from './pantheon/delegation-toast.ts'
|
|
33
|
-
import { FilePersistenceAdapter } from './pantheon/file-persistence.ts'
|
|
34
20
|
import { GOAL_LOOP_DEFAULTS, GoalLoop, GoalStore } from './pantheon/goal-loop.ts'
|
|
35
21
|
import { createReadEnhancer } from './pantheon/hashline/read-enhancer.ts'
|
|
36
22
|
import { createHashlineEditTool } from './pantheon/hashline/tool.ts'
|
|
@@ -38,14 +24,10 @@ import { createIdleDispatcher } from './pantheon/idle-continuation.ts'
|
|
|
38
24
|
import { createPantheonLogger } from './pantheon/logger.ts'
|
|
39
25
|
import { createModelCommand } from './pantheon/model-command.ts'
|
|
40
26
|
import { pantheonPluginOnce } from './pantheon/plugin-once.ts'
|
|
41
|
-
import {
|
|
42
|
-
applyActivePresetToConfig,
|
|
43
|
-
loadRoutingAgentModels,
|
|
44
|
-
loadRoutingMaxSteps,
|
|
45
|
-
loadRoutingPermissionTask,
|
|
46
|
-
} from './pantheon/presets.mjs'
|
|
27
|
+
import { applyActivePresetToConfig } from './pantheon/presets.mjs'
|
|
47
28
|
import { safeSessionPath } from './pantheon/session-guard.ts'
|
|
48
|
-
import {
|
|
29
|
+
import { getSharedBoard } from './pantheon/shared-board.ts'
|
|
30
|
+
import { createTaskResultGuard } from './pantheon/task-result-guard.ts'
|
|
49
31
|
import {
|
|
50
32
|
TODO_ENFORCER_DEFAULTS,
|
|
51
33
|
TodoEnforcer,
|
|
@@ -53,6 +35,7 @@ import {
|
|
|
53
35
|
todoEnforcerEnabledFromEnv,
|
|
54
36
|
} from './pantheon/todo-enforcer.ts'
|
|
55
37
|
import { TodoPreserver } from './pantheon/todo-preserve.ts'
|
|
38
|
+
import { handleToolCeilingEvent, type ToolCeilingResult } from './pantheon/tool-ceiling.ts'
|
|
56
39
|
import { checkTuiVersionStaleness } from './pantheon/tui-version-check.ts'
|
|
57
40
|
import { activePresetCandidates, createVisionHandler } from './pantheon/vision.ts'
|
|
58
41
|
|
|
@@ -64,24 +47,10 @@ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.t
|
|
|
64
47
|
// .pantheon/logs/hooks.log; the console echo is opt-in via PANTHEON_HOOKS_LOG=1.
|
|
65
48
|
const log = createPantheonLogger({ module: 'pantheon-plugin' })
|
|
66
49
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
// to zeusDelegateWithRetry explicitly.
|
|
72
|
-
const stepCapTracker = new StepCapTracker(loadRoutingMaxSteps({ logger: log }))
|
|
73
|
-
|
|
74
|
-
// O5: permission.task glob rules from routing.yml `permission.task`
|
|
75
|
-
// (fail-open → undefined = existing runtime matrix only).
|
|
76
|
-
const routingPermissionTask = loadRoutingPermissionTask({ logger: log })
|
|
77
|
-
|
|
78
|
-
const board = new BackgroundJobBoard({
|
|
79
|
-
maxConcurrentPerAgent: 3,
|
|
80
|
-
signalDir: '.pantheon/deepwork/board-signals',
|
|
81
|
-
})
|
|
82
|
-
const persistence = new FilePersistenceAdapter('.pantheon/board/state.json')
|
|
83
|
-
board.setPersistence(persistence)
|
|
84
|
-
let terminalToastClient: ToastClient = {}
|
|
50
|
+
// beta.5: the board is a PROCESS-wide singleton (getSharedBoard) shared with
|
|
51
|
+
// src/plugins/pantheon-hooks.ts, whose native-task mirror registers children
|
|
52
|
+
// here. globalThis anchoring dedupes the npm+repo double load.
|
|
53
|
+
const board = getSharedBoard()
|
|
85
54
|
board
|
|
86
55
|
.recoverRunningJobs()
|
|
87
56
|
.catch((err) => log.error('[Pantheon Plugin] Failed to recover running jobs:', err))
|
|
@@ -91,17 +60,14 @@ board
|
|
|
91
60
|
// file-only log line (console echo opt-in via PANTHEON_HOOKS_LOG). There is
|
|
92
61
|
// deliberately NO chat delivery — the user policy is zero delegation
|
|
93
62
|
// notifications in the transcript. Completion visibility lives in the board
|
|
94
|
-
// `[unread]` marker
|
|
95
|
-
//
|
|
96
|
-
// carry-forward.
|
|
63
|
+
// `[unread]` marker and the TUI toasts owned by pantheon-hooks
|
|
64
|
+
// (PANTHEON_TOASTS gate).
|
|
97
65
|
board.onTerminal((taskID: string) => {
|
|
98
66
|
const job = board.get(taskID)
|
|
99
67
|
if (!job) return
|
|
100
68
|
log.info(
|
|
101
69
|
`[Pantheon Plugin] Board terminal: [${job.alias}] ${job.description} → ${job.state}${job.resultSummary ? ` — ${job.resultSummary}` : ''}`,
|
|
102
70
|
)
|
|
103
|
-
// Deliberately direct-to-TUI: onTerminal must not inject chat/transcript text.
|
|
104
|
-
void showDelegationTerminalToast(terminalToastClient, job)
|
|
105
71
|
})
|
|
106
72
|
|
|
107
73
|
// Periodically prune terminal/reconciled jobs (24h TTL, every 30 min).
|
|
@@ -127,10 +93,11 @@ function isRootSession(sessionID: string): boolean {
|
|
|
127
93
|
return rootSessions.has(sessionID) || sessionHierarchy.isRoot(sessionID)
|
|
128
94
|
}
|
|
129
95
|
|
|
130
|
-
// The read-only registry is populated by
|
|
131
|
-
//
|
|
132
|
-
// The guard denies edit/write/bash/task
|
|
133
|
-
// from tool.execute.before; unknown sessions
|
|
96
|
+
// The read-only registry is populated by the chat.params hook below, when a
|
|
97
|
+
// session's active agent is read-only (apollo/gaia — READ_ONLY_AGENTS /
|
|
98
|
+
// syncReadOnlySession). The guard denies edit/write/bash/task/hashline_edit
|
|
99
|
+
// inside those sessions by throwing from tool.execute.before; unknown sessions
|
|
100
|
+
// are allowed (safe default).
|
|
134
101
|
const enforcementGuard = createEnforcementGuard({
|
|
135
102
|
getReadOnlySessions: () => readOnlyRegistry.sessionIDs(),
|
|
136
103
|
options: {
|
|
@@ -148,10 +115,7 @@ const enforcementGuard = createEnforcementGuard({
|
|
|
148
115
|
// to the shared hooks.log for @nyx frequency measurement.
|
|
149
116
|
const commandNormalizer = createCommandNormalizer({ logger: log })
|
|
150
117
|
|
|
151
|
-
//
|
|
152
|
-
const COMPACTION_MAX_ITEMS = 10
|
|
153
|
-
|
|
154
|
-
// Root-session registry for the delegation depth guard: sessions created
|
|
118
|
+
// Root-session registry for the read-only enforcement guard: sessions created
|
|
155
119
|
// without a parentID are roots. Populated from TWO sources:
|
|
156
120
|
// 1. the `session.created` event hook (live roots — fires for sessions
|
|
157
121
|
// created while the plugin runs);
|
|
@@ -159,19 +123,30 @@ const COMPACTION_MAX_ITEMS = 10
|
|
|
159
123
|
// hook (compaction-134) — opencode does NOT replay session.created
|
|
160
124
|
// events for sessions that exist before plugin load, so without the
|
|
161
125
|
// seed a resumed root would never enter this set after a restart.
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// works even when this set is incomplete.
|
|
126
|
+
// The guard treats unknown sessions as roots, so this set is an enrichment
|
|
127
|
+
// that improves accuracy but is never required for correctness.
|
|
165
128
|
const rootSessions = new Set<string>()
|
|
166
129
|
const sessionHierarchy = new SessionHierarchyRegistry()
|
|
167
130
|
const sessionAgents = new Map<string, string>()
|
|
131
|
+
const sessionToolCeilings = new Map<string, ToolCeilingResult>()
|
|
132
|
+
|
|
133
|
+
/** Pure helper: collect the IDs of root sessions (parentID === undefined). */
|
|
134
|
+
function collectRootSessionIDs(
|
|
135
|
+
sessions: ReadonlyArray<{ id: string; parentID?: string }>,
|
|
136
|
+
): Set<string> {
|
|
137
|
+
const roots = new Set<string>()
|
|
138
|
+
for (const session of sessions) {
|
|
139
|
+
if (session.parentID === undefined) roots.add(session.id)
|
|
140
|
+
}
|
|
141
|
+
return roots
|
|
142
|
+
}
|
|
168
143
|
|
|
169
144
|
/**
|
|
170
145
|
* Seed resumed root sessions without making OpenCode startup depend on the
|
|
171
146
|
* session API responding. Some OpenCode environments do not have the API
|
|
172
147
|
* ready while plugin config hooks run; awaiting session.list() there leaves
|
|
173
|
-
* the TUI stuck before it can open. The
|
|
174
|
-
*
|
|
148
|
+
* the TUI stuck before it can open. The enforcement guard treats an unknown
|
|
149
|
+
* session as a root, so this enrichment is deliberately best-effort.
|
|
175
150
|
*/
|
|
176
151
|
function seedRootSessionsInBackground(input: PluginInput): void {
|
|
177
152
|
// Fail-open: environments without the session API (client.session absent or
|
|
@@ -208,62 +183,6 @@ function sdkErrorMessage(error: { data?: { message?: string } } | null | undefin
|
|
|
208
183
|
return typeof message === 'string' && message.trim() !== '' ? message : 'request failed'
|
|
209
184
|
}
|
|
210
185
|
|
|
211
|
-
/**
|
|
212
|
-
* Adapt the opencode SDK client to the structural DelegationClient the
|
|
213
|
-
* delegation toolset expects: the SDK wraps every call in a
|
|
214
|
-
* `{data, error, request, response}` result while delegation.ts uses direct
|
|
215
|
-
* returns. Errors surface as throws so the toolset's own error handling
|
|
216
|
-
* (tool errors returned as TEXT) keeps working.
|
|
217
|
-
*/
|
|
218
|
-
function adaptDelegationClient(client: PluginInput['client']): DelegationClient {
|
|
219
|
-
return {
|
|
220
|
-
session: {
|
|
221
|
-
create: async (input) => {
|
|
222
|
-
const body: {
|
|
223
|
-
parentID?: string
|
|
224
|
-
title?: string
|
|
225
|
-
model?: { id: string; providerID: string }
|
|
226
|
-
} = { parentID: input.body.parentID }
|
|
227
|
-
if (input.body.title !== undefined) body.title = input.body.title
|
|
228
|
-
// Forward a model only when the active routing profile supplied one.
|
|
229
|
-
if (input.body.model !== undefined) body.model = input.body.model
|
|
230
|
-
const result = await client.session.create({ body })
|
|
231
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
232
|
-
return { id: result.data.id }
|
|
233
|
-
},
|
|
234
|
-
promptAsync: async (input) => {
|
|
235
|
-
const path = safeSessionPath(input.path.id)
|
|
236
|
-
if (!path) {
|
|
237
|
-
log.warn(`[Pantheon Plugin] promptAsync skipped: invalid sessionID "${input.path.id}"`)
|
|
238
|
-
throw new Error(`invalid sessionID: ${input.path.id}`)
|
|
239
|
-
}
|
|
240
|
-
const result = await client.session.promptAsync({
|
|
241
|
-
path: path.path,
|
|
242
|
-
body: {
|
|
243
|
-
agent: input.body.agent,
|
|
244
|
-
...(input.body.model !== undefined
|
|
245
|
-
? { model: { providerID: input.body.model.providerID, modelID: input.body.model.id } }
|
|
246
|
-
: {}),
|
|
247
|
-
parts: input.body.parts,
|
|
248
|
-
},
|
|
249
|
-
})
|
|
250
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
251
|
-
return result.data
|
|
252
|
-
},
|
|
253
|
-
messages: async (input) => {
|
|
254
|
-
const path = safeSessionPath(input.path.id)
|
|
255
|
-
if (!path) {
|
|
256
|
-
log.warn(`[Pantheon Plugin] messages skipped: invalid sessionID "${input.path.id}"`)
|
|
257
|
-
return []
|
|
258
|
-
}
|
|
259
|
-
const result = await client.session.messages({ path: path.path })
|
|
260
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
261
|
-
return result.data
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
186
|
/**
|
|
268
187
|
* Adapt the opencode SDK client to the structural TodoEnforcerClient the
|
|
269
188
|
* todo enforcer expects: unwraps the SDK `{data, error, request, response}`
|
|
@@ -344,7 +263,6 @@ function adaptTodoEnforcerClient(client: PluginInput['client']): TodoEnforcerCli
|
|
|
344
263
|
* Helpers live in src/pantheon/vision.ts and are imported from there directly.
|
|
345
264
|
*/
|
|
346
265
|
const plugin: Plugin = async (input: PluginInput) => {
|
|
347
|
-
terminalToastClient = input.client as unknown as ToastClient
|
|
348
266
|
// release-134: runtime guard against DOUBLE registration (duplicate toasts +
|
|
349
267
|
// duplicate task_ids in one process — 2026-08-12 live logs). opencode
|
|
350
268
|
// merges the global config (npm-package plugin paths) with the project
|
|
@@ -359,100 +277,11 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
359
277
|
// is older than the installed package. Runs once, fail-open, no auto-sync.
|
|
360
278
|
checkTuiVersionStaleness((msg) => log.warn(msg))
|
|
361
279
|
const vision = createVisionHandler(input)
|
|
362
|
-
//
|
|
363
|
-
// hook.
|
|
364
|
-
// (
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// plugin/frontmatter; these are enforced by delegation.ts itself.
|
|
368
|
-
const delegationBudgets = resolveDelegationBudgets()
|
|
369
|
-
const wallClockTimeoutMs = resolveDelegationTimeoutMs()
|
|
370
|
-
|
|
371
|
-
// WS1 (PR #94): delegation backend gate. Default = legacy (the V1
|
|
372
|
-
// fire-and-forget toolset, intact); PANTHEON_DELEGATE_MODE=native selects
|
|
373
|
-
// the thin manager over native task() semantics. Kill-switch
|
|
374
|
-
// PANTHEON_DELEGATION=off covers the native manager; finalizeDelegation stays on
|
|
375
|
-
// the legacy observer: it works on the shared board in both modes and must
|
|
376
|
-
// never be kill-switched, so children never orphan. The idle hook below is
|
|
377
|
-
// untouched.
|
|
378
|
-
const delegationClient = adaptDelegationClient(input.client)
|
|
379
|
-
// Only an explicitly active routing profile may provide child models.
|
|
380
|
-
// An empty map deliberately lets OpenCode inherit the parent model.
|
|
381
|
-
const routingAgentModels = loadRoutingAgentModels({
|
|
382
|
-
candidates: activePresetCandidates(),
|
|
383
|
-
logger: log,
|
|
384
|
-
})
|
|
385
|
-
const readOnlyAgents = new Set(['apollo', 'gaia'])
|
|
386
|
-
const legacyDelegation = createDelegationTools({
|
|
387
|
-
board,
|
|
388
|
-
client: delegationClient,
|
|
389
|
-
options: {
|
|
390
|
-
rootSessions,
|
|
391
|
-
isChildSession: (sessionID) => sessionHierarchy.isChild(sessionID),
|
|
392
|
-
registerChildSession: (sessionID, parentID) => {
|
|
393
|
-
sessionHierarchy.register({ id: sessionID, parentID })
|
|
394
|
-
},
|
|
395
|
-
enforceRuntimeMatrix: true,
|
|
396
|
-
readOnlyAgents,
|
|
397
|
-
agentModels: routingAgentModels,
|
|
398
|
-
delegationBudgets,
|
|
399
|
-
stepCapTracker,
|
|
400
|
-
...(routingPermissionTask != null ? { permissionTask: routingPermissionTask } : {}),
|
|
401
|
-
...(wallClockTimeoutMs !== undefined ? { wallClockTimeoutMs } : {}),
|
|
402
|
-
},
|
|
403
|
-
})
|
|
404
|
-
const delegateMode = resolveDelegateMode()
|
|
405
|
-
if (delegateMode === 'native') {
|
|
406
|
-
log.info('[Pantheon Plugin] Delegation mode: native (PANTHEON_DELEGATE_MODE=native)')
|
|
407
|
-
}
|
|
408
|
-
const delegation =
|
|
409
|
-
delegateMode === 'native'
|
|
410
|
-
? buildPluginNativeDelegation({
|
|
411
|
-
board,
|
|
412
|
-
client: delegationClient,
|
|
413
|
-
isRootSession,
|
|
414
|
-
registerChildSession: (sessionID, parentID) => {
|
|
415
|
-
sessionHierarchy.register({ id: sessionID, parentID })
|
|
416
|
-
},
|
|
417
|
-
registerReadOnlySession: (sessionID, info) => {
|
|
418
|
-
readOnlyRegistry.register(sessionID, info)
|
|
419
|
-
},
|
|
420
|
-
readOnlyAgents,
|
|
421
|
-
agentModels: routingAgentModels,
|
|
422
|
-
stepCap: stepCapTracker,
|
|
423
|
-
...(wallClockTimeoutMs !== undefined ? { wallClockTimeoutMs } : {}),
|
|
424
|
-
finalizeDelegation: legacyDelegation.finalizeDelegation,
|
|
425
|
-
})
|
|
426
|
-
: legacyDelegation
|
|
427
|
-
|
|
428
|
-
// Proactive idle-child scan (Fix 2): finalize children that are idle on the
|
|
429
|
-
// board but have no MD report on disk — eliminates the phantom "running"
|
|
430
|
-
// window. Runs every 30s (configurable via PANTHEON_IDLE_SCAN_INTERVAL_MS),
|
|
431
|
-
// unref'd so it never holds the process open. Fail-open by design.
|
|
432
|
-
const idleScanIntervalMs = Number(process.env.PANTHEON_IDLE_SCAN_INTERVAL_MS) || undefined
|
|
433
|
-
startIdleChildScan(
|
|
434
|
-
{
|
|
435
|
-
board,
|
|
436
|
-
finalize: (childSessionID, opts) => delegation.finalizeDelegation(childSessionID, opts),
|
|
437
|
-
hasReport: (job) => {
|
|
438
|
-
try {
|
|
439
|
-
const filePath = join('.pantheon/delegations', job.parentSessionID, `${job.alias}.md`)
|
|
440
|
-
return existsSync(filePath)
|
|
441
|
-
} catch {
|
|
442
|
-
return false
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
logger: log,
|
|
446
|
-
},
|
|
447
|
-
idleScanIntervalMs,
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
// Wave 1 (PR #46): TODO continuation enforcer for root/non-board sessions.
|
|
451
|
-
// Mirrors routing.yml `todo_enforcer` (plugin scope has no routing.yml
|
|
452
|
-
// access — COMPACTION_MAX_ITEMS pattern). The event hook routes non-board
|
|
453
|
-
// session.idle events here; board-child idles go to finalizeDelegation.
|
|
454
|
-
// Runtime kill-switch: PANTHEON_TODO_ENFORCER=off (routing.yml is a doc
|
|
455
|
-
// mirror only — the env var is the real switch, PANTHEON_TOASTS pattern).
|
|
280
|
+
// Wave 1 (PR #46): TODO continuation enforcer for root sessions. The event
|
|
281
|
+
// hook routes `session.idle` events here; an active goal owns the idle
|
|
282
|
+
// (goal loop), otherwise the todo enforcer re-injects. Runtime kill-switch:
|
|
283
|
+
// PANTHEON_TODO_ENFORCER=off (routing.yml is a doc mirror only — the env var
|
|
284
|
+
// is the real switch, PANTHEON_TOASTS pattern).
|
|
456
285
|
const todoEnforcer = new TodoEnforcer({
|
|
457
286
|
client: adaptTodoEnforcerClient(input.client),
|
|
458
287
|
board,
|
|
@@ -504,15 +333,18 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
504
333
|
},
|
|
505
334
|
}
|
|
506
335
|
const sandboxHandler = createContextSandbox(sandboxConfig)
|
|
336
|
+
// Task Result Guard (2026-09-10): detect empty task() results from native
|
|
337
|
+
// opencode subagent calls and convert to explicit errors. Runs FIRST in the
|
|
338
|
+
// chain so the error message is visible even if later handlers skip.
|
|
339
|
+
const taskResultGuard = createTaskResultGuard()
|
|
507
340
|
const combinedAfter: typeof readEnhancer = async (input, output) => {
|
|
341
|
+
await taskResultGuard(input, output)
|
|
508
342
|
await sandboxHandler(input, output)
|
|
509
343
|
await readEnhancer(input, output)
|
|
510
344
|
}
|
|
511
345
|
|
|
512
346
|
// Wave 4 (PR #46): /cost — delegation cost + token visibility. Reads
|
|
513
|
-
// opencode.db read-only
|
|
514
|
-
// Fully wired (unlike dispatch-guard, which is manual-orchestration-only
|
|
515
|
-
// because opencode 1.18.x cannot intercept task completion via hooks).
|
|
347
|
+
// opencode.db read-only via the single node:sqlite backend. Fully wired.
|
|
516
348
|
const costCommand = createCostCommand()
|
|
517
349
|
const modelCommand = createModelCommand()
|
|
518
350
|
|
|
@@ -521,11 +353,11 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
521
353
|
// compaction-134 seed: opencode does NOT replay `session.created`
|
|
522
354
|
// events for sessions that exist before plugin load, so after a
|
|
523
355
|
// restart the resumed ROOT session never enters rootSessions and the
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
//
|
|
356
|
+
// read-only enforcement guard would misclassify it. Seed the registry
|
|
357
|
+
// from session.list() in the background — every session WITHOUT a
|
|
358
|
+
// parentID is a root. Fail-open: a failed/throttled list leaves the set
|
|
359
|
+
// untouched (the guard treats unknown sessions as roots), and config
|
|
360
|
+
// setup must never wait for the session API.
|
|
529
361
|
// Context sandbox config — opencode.json → context_sandbox: {enabled, limits}
|
|
530
362
|
try {
|
|
531
363
|
const raw = (config as unknown as Record<string, unknown>).context_sandbox
|
|
@@ -570,12 +402,9 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
570
402
|
log.warn('[plugin] preset application failed (see logs for details)')
|
|
571
403
|
}
|
|
572
404
|
},
|
|
573
|
-
//
|
|
574
|
-
//
|
|
405
|
+
// Plugin tool surface: tag-anchored edits (hashline_edit), the full-auto
|
|
406
|
+
// goal loop, /cost and /pantheon-model management.
|
|
575
407
|
tool: {
|
|
576
|
-
pantheon_delegate: delegation.pantheon_delegate,
|
|
577
|
-
pantheon_delegation_read: delegation.pantheon_delegation_read,
|
|
578
|
-
pantheon_delegation_list: delegation.pantheon_delegation_list,
|
|
579
408
|
hashline_edit: hashlineEdit,
|
|
580
409
|
// Wave 3 (PR #46): full-auto goal tools (single active goal/session).
|
|
581
410
|
pantheon_goal_create: goalTools.pantheon_goal_create,
|
|
@@ -598,6 +427,10 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
598
427
|
// The SDK exposes the active agent here; tool.execute.before has no
|
|
599
428
|
// agent field. Never infer identity from a prompt or tool name.
|
|
600
429
|
sessionAgents.set(hookInput.sessionID, hookInput.agent)
|
|
430
|
+
// Phase 4: chat.params is the ONLY hook with the active agent, so this
|
|
431
|
+
// is where the read-only registry is populated — apollo/gaia sessions
|
|
432
|
+
// must be denied edit/write/bash/task/hashline_edit by the guard.
|
|
433
|
+
syncReadOnlySession(readOnlyRegistry, hookInput.sessionID, hookInput.agent)
|
|
601
434
|
},
|
|
602
435
|
'experimental.chat.messages.transform': vision.messagesTransform,
|
|
603
436
|
event: async ({ event: ev }) => {
|
|
@@ -609,33 +442,27 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
609
442
|
if (info.parentID === undefined) rootSessions.add(info.id)
|
|
610
443
|
}
|
|
611
444
|
}
|
|
445
|
+
// Phase 4: a deleted session must not leave a read-only registration
|
|
446
|
+
// (or cached agent identity) behind.
|
|
447
|
+
if (ev.type === 'session.deleted') {
|
|
448
|
+
readOnlyRegistry.unregister(ev.properties.info.id)
|
|
449
|
+
sessionAgents.delete(ev.properties.info.id)
|
|
450
|
+
}
|
|
612
451
|
// release-134 Phase 3: activate the todo snapshot captured at
|
|
613
452
|
// compacting time — the first todowrite within the restore window is
|
|
614
453
|
// rewritten with the exact list (see todo-preserve.ts). Fail-open.
|
|
615
454
|
if (ev.type === 'session.compacted') {
|
|
616
455
|
await todoPreserver.onCompacted(ev.properties.sessionID)
|
|
617
456
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
// Wave 1/3: non-board idle → idle dispatcher. Board children are
|
|
628
|
-
// handled by handleDelegationEvent above; everything else (roots,
|
|
629
|
-
// non-board sessions) is routed by priority: an active goal owns the
|
|
630
|
-
// idle (goal loop), otherwise the todo enforcer re-injects. Neither
|
|
631
|
-
// onIdle ever throws (internal failures are logged + swallowed), so
|
|
632
|
-
// the session is safe.
|
|
633
|
-
if (!delegated && ev.type === 'session.idle') {
|
|
634
|
-
await idleDispatcher.onIdle(ev.properties.sessionID)
|
|
635
|
-
}
|
|
636
|
-
} catch (err) {
|
|
637
|
-
// The event hook must never break the session.
|
|
638
|
-
log.error('[Pantheon Plugin] Delegation event handling failed:', err)
|
|
457
|
+
if (ev.type === 'message.updated') {
|
|
458
|
+
await handleToolCeilingEvent(input.client, ev, sessionToolCeilings)
|
|
459
|
+
}
|
|
460
|
+
// Wave 1/3: `session.idle` → idle dispatcher. An active goal owns the
|
|
461
|
+
// idle (goal loop); otherwise the todo enforcer re-injects. onIdle never
|
|
462
|
+
// throws (internal failures are logged + swallowed), so the session is
|
|
463
|
+
// safe.
|
|
464
|
+
if (ev.type === 'session.idle') {
|
|
465
|
+
await idleDispatcher.onIdle(ev.properties.sessionID)
|
|
639
466
|
}
|
|
640
467
|
await vision.event({ event: ev })
|
|
641
468
|
},
|
|
@@ -661,16 +488,14 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
661
488
|
// through untouched.
|
|
662
489
|
'tool.execute.after': combinedAfter,
|
|
663
490
|
// Phase 4 + release-134 Phase 2: keep the session's working state across
|
|
664
|
-
// compaction — preservation directive, active goals (<mission_context>)
|
|
665
|
-
// pending todos (<todo_context>)
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
// break the experimental compaction hook.
|
|
491
|
+
// compaction — preservation directive, active goals (<mission_context>)
|
|
492
|
+
// and pending todos (<todo_context>). Guarded: only pushes when there is
|
|
493
|
+
// something to preserve; a build failure must never break the experimental
|
|
494
|
+
// compaction hook.
|
|
669
495
|
'experimental.session.compacting': async (_input, output) => {
|
|
670
496
|
try {
|
|
671
|
-
const blocks = await buildCompactionContext(
|
|
497
|
+
const blocks = await buildCompactionContext({
|
|
672
498
|
sessionID: _input.sessionID,
|
|
673
|
-
maxItems: COMPACTION_MAX_ITEMS,
|
|
674
499
|
goals: {
|
|
675
500
|
enabled: GOAL_LOOP_DEFAULTS.enabled,
|
|
676
501
|
list: (sessionID: string) => goalStore.list(sessionID),
|
|
@@ -680,6 +505,13 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
680
505
|
list: (sessionID: string) => todoEnforcer.listPendingTodos(sessionID),
|
|
681
506
|
},
|
|
682
507
|
})
|
|
508
|
+
const ceiling = sessionToolCeilings.get(_input.sessionID)
|
|
509
|
+
if (ceiling?.status === 'OK') {
|
|
510
|
+
output.context.push(
|
|
511
|
+
`<tool_ceiling remaining_context_tokens="${ceiling.ceiling}">` +
|
|
512
|
+
'Use the remaining host-reported context budget when selecting tools.</tool_ceiling>',
|
|
513
|
+
)
|
|
514
|
+
}
|
|
683
515
|
if (blocks.length > 0) {
|
|
684
516
|
output.context.push(...blocks)
|
|
685
517
|
}
|
|
@@ -177,6 +177,7 @@ import { appendFile, mkdir } from 'node:fs/promises'
|
|
|
177
177
|
import { join } from 'node:path'
|
|
178
178
|
import type { Plugin, PluginInput } from '@opencode-ai/plugin'
|
|
179
179
|
import { pantheonPluginOnce } from '../pantheon/plugin-once.ts'
|
|
180
|
+
import { getSharedBoard } from '../pantheon/shared-board.ts'
|
|
180
181
|
import { type HookPayload, type HookResult, runHook } from './hook-runner.ts'
|
|
181
182
|
|
|
182
183
|
/** Tools that represent a subagent delegation (opencode `task` tool etc.). */
|
|
@@ -775,6 +776,51 @@ function delegationAgent(tool: string, args?: unknown): string {
|
|
|
775
776
|
return tool
|
|
776
777
|
}
|
|
777
778
|
|
|
779
|
+
/**
|
|
780
|
+
* beta.5: mirror a native background task dispatch onto the SHARED board
|
|
781
|
+
* (getSharedBoard) so the existing finalize path in plugin.ts (session.idle
|
|
782
|
+
* event + 30s idle scan) writes the terminal report and the TUI delegation
|
|
783
|
+
* panel tracks the child end-to-end. Fire-and-forget and silent: a mirror
|
|
784
|
+
* failure must never surface into the tool call or the transcript.
|
|
785
|
+
* registerLaunchIfAbsent keeps it idempotent against any other dispatcher
|
|
786
|
+
* (plugin double-load, repeated after-hook firings).
|
|
787
|
+
*/
|
|
788
|
+
function mirrorNativeBackgroundDispatch(
|
|
789
|
+
input: { tool: string; sessionID: string },
|
|
790
|
+
args: unknown,
|
|
791
|
+
result: ToolExecuteAfterResult | null,
|
|
792
|
+
): void {
|
|
793
|
+
const taskID = extractTaskId(result)
|
|
794
|
+
if (taskID === '' || typeof input.sessionID !== 'string' || input.sessionID === '') return
|
|
795
|
+
const description = trim(delegationDescription(args) ?? `native ${input.tool} dispatch`, 200)
|
|
796
|
+
try {
|
|
797
|
+
getSharedBoard()
|
|
798
|
+
.registerLaunchIfAbsent({
|
|
799
|
+
taskID,
|
|
800
|
+
parentSessionID: input.sessionID,
|
|
801
|
+
agent: delegationAgent(input.tool, args),
|
|
802
|
+
description,
|
|
803
|
+
})
|
|
804
|
+
.catch(() => {
|
|
805
|
+
// Mirror is best-effort — never surface into the tool call.
|
|
806
|
+
})
|
|
807
|
+
} catch {
|
|
808
|
+
// Never let the mirror take down the tool call.
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Task description from task-tool args (description ?? prompt); undefined when absent. */
|
|
813
|
+
function delegationDescription(args: unknown): string | undefined {
|
|
814
|
+
try {
|
|
815
|
+
const a = (args ?? {}) as Record<string, unknown>
|
|
816
|
+
const raw = a.description ?? a.prompt
|
|
817
|
+
if (typeof raw === 'string' && raw.trim() !== '') return raw
|
|
818
|
+
} catch {
|
|
819
|
+
// Malformed args — fall through to the generic description.
|
|
820
|
+
}
|
|
821
|
+
return undefined
|
|
822
|
+
}
|
|
823
|
+
|
|
778
824
|
/** 🚀 delegation-started toast (variant 'info', 2500ms), deduped per agent. */
|
|
779
825
|
async function delegationStartedToast(ctx: HookContext, agent: string): Promise<void> {
|
|
780
826
|
await throttledToast(ctx, {
|
|
@@ -1311,6 +1357,11 @@ const plugin: Plugin = async ({ client, directory }) => {
|
|
|
1311
1357
|
// NOT a completion: do not mark the group done and do not fire ✅
|
|
1312
1358
|
// — that would corrupt olympians aggregation. Real completion of
|
|
1313
1359
|
// background tasks is unobservable on 1.18.13 (see header).
|
|
1360
|
+
// beta.5: mirror the native background child onto the SHARED
|
|
1361
|
+
// board so the existing finalize path (session.idle + idle scan
|
|
1362
|
+
// in plugin.ts) writes the terminal report and the TUI
|
|
1363
|
+
// delegation panel tracks the child end-to-end. Fire-and-forget.
|
|
1364
|
+
mirrorNativeBackgroundDispatch(input, args, result)
|
|
1314
1365
|
} else {
|
|
1315
1366
|
const handledByOlympians = handleOlympiansCompletion(ctx, agent)
|
|
1316
1367
|
if (!handledByOlympians) void delegationDoneToast(ctx, agent)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;;;;;iBAYwB"}
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;;;;;iBAYwB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * No-op server stub for the opencode plugin loader.\n *\n * The opencode plugin host requires every plugin (TUI included) to expose a\n * `server()` entry via `exports[\"./server\"]` (or package.json `main`). When\n * that entry is missing the host fails to attach the plugin's server half and\n * the TUI runtime never wires the reactive channel — the initial JSX mounts\n * but effects/refresh/events stay dead (the \"mudo panel\" symptom).\n *\n * This stub satisfies the loader contract; pantheon-tui is TUI-only and has\n * no background server work of its own.\n */\nexport default function server() {\n return {}\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAwB,SAAS;CAC/B,OAAO,CAAC;AACV"}
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * No-op server stub for the opencode plugin loader.\n *\n * The opencode plugin host requires every plugin (TUI included) to expose a\n * `server()` entry via `exports[\"./server\"]` (or package.json `main`). When\n * that entry is missing the host fails to attach the plugin's server half and\n * the TUI runtime never wires the reactive channel — the initial JSX mounts\n * but effects/refresh/events stay dead (the \"mudo panel\" symptom).\n *\n * This stub satisfies the loader contract; pantheon-tui is TUI-only and has\n * no background server work of its own.\n */\nexport default function server() {\n return {}\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAwB,SAAS;CAC/B,OAAO,CAAC;AACV"}
|