pantheon-opencode 1.4.1 → 1.4.3
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/README.md +175 -147
- package/bin/pantheon-init.mjs +7 -0
- package/commands/pantheon-model.md +34 -0
- package/package.json +2 -2
- package/scripts/generate-preset-docs.mjs +239 -0
- package/scripts/install/model-picker.mjs +367 -2
- package/scripts/install/opencode.mjs +150 -137
- package/src/agents/zeus.md +14 -25
- package/src/pantheon/cost-command.ts +7 -7
- package/src/pantheon/delegation-enforce.ts +12 -13
- package/src/pantheon/goal-loop.ts +2 -2
- package/src/pantheon/goal-store.ts +1 -1
- package/src/pantheon/hashline/tool.ts +2 -2
- package/src/pantheon/model-command.ts +911 -0
- package/src/pantheon/presets.mjs +30 -33
- package/src/pantheon/tool-context.ts +41 -0
- package/src/plugin.ts +45 -193
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/dist/tui.tsx +13 -2
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/plugins/tui/src/index.tsx +21 -8
- package/src/plugins/tui/tsdown.config.ts +11 -0
- package/src/routing.yml +71 -123
- package/src/pantheon/delegation-classifier.ts +0 -307
- package/src/pantheon/delegation-finalize.ts +0 -374
- package/src/pantheon/delegation-notify.ts +0 -197
- package/src/pantheon/delegation-toast.ts +0 -73
- package/src/pantheon/delegation.ts +0 -1110
- package/src/pantheon/permission-eval.ts +0 -284
- package/src/pantheon/step-cap.ts +0 -110
- package/src/pantheon/zeus-delegate-with-retry.ts +0 -401
package/src/pantheon/presets.mjs
CHANGED
|
@@ -33,12 +33,13 @@ export const EFFORT_RANK = { low: 0, medium: 1, high: 2 }
|
|
|
33
33
|
/**
|
|
34
34
|
* Capability table: reasoning-effort ceiling + image-input (vision) support.
|
|
35
35
|
*
|
|
36
|
-
* `vision` flags mark models that accept image input. Verified
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
36
|
+
* `vision` flags mark models that accept image input. Verified 2026-08-27:
|
|
37
|
+
* deepseek-v4-pro high, deepseek-v4-flash medium text-only; mimo-v2.5 /
|
|
38
|
+
* mimo-v2.5-free multimodal low; qwen3.6-plus-free / qwen3.8-max text-only;
|
|
39
|
+
* glm-5.3-flash high text-only; kimi-k2.7-code high, kimi-k3 medium text-only;
|
|
40
|
+
* nemotron-3-super-free high text-only; big-pickle medium text-only;
|
|
41
|
+
* minimax-m3 multimodal high; gpt-5.6 family native image support.
|
|
42
|
+
* Removed: minimax-m2.5, kimi-k2.5, glm-5.1/5.2, ox-alpha (deprecated 2026).
|
|
42
43
|
*/
|
|
43
44
|
export const CAPABILITY_TABLE = [
|
|
44
45
|
{ prefix: 'deepseek/deepseek-v4-pro', maxEffort: 'high', stripEffort: false, vision: false },
|
|
@@ -60,31 +61,18 @@ export const CAPABILITY_TABLE = [
|
|
|
60
61
|
// entry above matches 'mimo/v2.5' (full-string) but NOT 'mimo-v2.5'
|
|
61
62
|
// (segment 'mimo-v2.5' has no slash, so 'mimo/' prefix cannot match).
|
|
62
63
|
{ prefix: 'mimo-v2.5', maxEffort: 'low', stripEffort: false, vision: true },
|
|
63
|
-
// mimo-v2.5-pro is text-only per models.dev api.json (2026-08-
|
|
64
|
-
// Explicit entry needed: the 'mimo-v2.5' prefix substring-matches
|
|
65
|
-
// 'mimo-v2.5-pro' (longest-prefix wins), which would wrongly report
|
|
66
|
-
// vision: true for a text-only model.
|
|
64
|
+
// mimo-v2.5-pro is text-only per models.dev api.json (2026-08-27).
|
|
67
65
|
{ prefix: 'mimo-v2.5-pro', maxEffort: 'low', stripEffort: false, vision: false },
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{ prefix: '
|
|
73
|
-
{ prefix: '
|
|
74
|
-
|
|
75
|
-
// qwen3.7-max is text-only per models.dev api.json (2026-08-02); the
|
|
76
|
-
// multimodal variant is qwen3.7-plus.
|
|
66
|
+
// Go subscription specific — bare segment prefixes so opencode-go/<model> IDs match
|
|
67
|
+
{ prefix: 'glm-5.3-flash', maxEffort: 'high', stripEffort: false, vision: false },
|
|
68
|
+
{ prefix: 'kimi-k2.7-code', maxEffort: 'high', stripEffort: false, vision: false },
|
|
69
|
+
{ prefix: 'kimi-k3', maxEffort: 'medium', stripEffort: false, vision: false },
|
|
70
|
+
{ prefix: 'qwen3.8-max', maxEffort: 'high', stripEffort: false, vision: false },
|
|
71
|
+
{ prefix: 'qwen3.6-plus-free', maxEffort: 'medium', stripEffort: false, vision: false },
|
|
72
|
+
// Legacy qwen3.7 retained for capability checks (not used in new presets)
|
|
77
73
|
{ prefix: 'qwen3.7-max', maxEffort: 'high', stripEffort: false, vision: false },
|
|
78
|
-
// qwen3.7-plus IS multimodal (models.dev input: text+image) but image
|
|
79
|
-
// turns through the OpenCode Go gateway return HTTP 500 (opencode#33942
|
|
80
|
-
// + #29956). Flag stays true per models.dev; presets must NOT use it as a
|
|
81
|
-
// vision fallback — minimax-m3 is the confirmed opencode-go fallback.
|
|
82
74
|
{ prefix: 'qwen3.7-plus', maxEffort: 'medium', stripEffort: false, vision: true },
|
|
83
|
-
// minimax-m2.7 / minimax-m2.5 are text-only per models.dev api.json
|
|
84
|
-
// (2026-08-02). minimax-m3 is the vision-capable MiniMax on opencode-go
|
|
85
|
-
// (confirmed multimodal via the Go gateway, opencode#29956).
|
|
86
75
|
{ prefix: 'minimax-m2.7', maxEffort: 'medium', stripEffort: false, vision: false },
|
|
87
|
-
{ prefix: 'minimax-m2.5', maxEffort: 'medium', stripEffort: false, vision: false },
|
|
88
76
|
{ prefix: 'minimax-m3', maxEffort: 'high', stripEffort: false, vision: true },
|
|
89
77
|
// BARE prefix — needed for opencode-go/deepseek-v4-flash (segment match);
|
|
90
78
|
// provider-scoped deepseek/deepseek-v4-flash entry still wins by length
|
|
@@ -94,9 +82,10 @@ export const CAPABILITY_TABLE = [
|
|
|
94
82
|
// provider-scoped deepseek/deepseek-v4-pro entry still wins by length.
|
|
95
83
|
{ prefix: 'deepseek-v4-pro', maxEffort: 'high', stripEffort: false, vision: false },
|
|
96
84
|
// opencode (Zen free tier) — bare segment prefixes for opencode/<model>.
|
|
97
|
-
// big-pickle / nemotron-3-
|
|
98
|
-
// text-only via models.dev (2026-08-
|
|
85
|
+
// big-pickle / nemotron-3-super-free / north-mini-code-free verified
|
|
86
|
+
// text-only via models.dev (2026-08-27).
|
|
99
87
|
{ prefix: 'big-pickle', maxEffort: 'medium', stripEffort: false, vision: false },
|
|
88
|
+
{ prefix: 'nemotron-3-super-free', maxEffort: 'high', stripEffort: false, vision: false },
|
|
100
89
|
{ prefix: 'nemotron-3-ultra-free', maxEffort: 'high', stripEffort: false, vision: false },
|
|
101
90
|
{ prefix: 'north-mini-code-free', maxEffort: 'high', stripEffort: false, vision: false },
|
|
102
91
|
]
|
|
@@ -513,8 +502,13 @@ function providerDefKeyConfigured(provider, env) {
|
|
|
513
502
|
if (!provider || typeof provider !== 'object') return true
|
|
514
503
|
const envVar = provider.apiKeyEnv
|
|
515
504
|
if (typeof envVar !== 'string' || envVar === '') return true
|
|
516
|
-
const
|
|
517
|
-
|
|
505
|
+
const candidates =
|
|
506
|
+
envVar === 'PANTHEON_OPENCODE_API_KEY' ? [envVar, 'OPENCODE_GO_API_KEY'] : [envVar]
|
|
507
|
+
for (const candidate of candidates) {
|
|
508
|
+
const key = env[candidate]
|
|
509
|
+
if (typeof key === 'string' && key.trim() !== '') return true
|
|
510
|
+
}
|
|
511
|
+
return false
|
|
518
512
|
}
|
|
519
513
|
|
|
520
514
|
/**
|
|
@@ -532,7 +526,6 @@ function providerDefKeyConfigured(provider, env) {
|
|
|
532
526
|
* @returns {string|undefined} missing env var name, or undefined when usable
|
|
533
527
|
*/
|
|
534
528
|
export function missingProviderKeyEnv(providerID, { env = process.env, routingPath } = {}) {
|
|
535
|
-
if (providerID === 'opencode-go') return undefined
|
|
536
529
|
const defs = loadPresetDefs(routingPath)
|
|
537
530
|
for (const def of Object.values(defs)) {
|
|
538
531
|
const provider = def?.providers?.[providerID]
|
|
@@ -583,7 +576,11 @@ export function applyPreset(config, resolved, { env = process.env } = {}) {
|
|
|
583
576
|
config.provider[id] ??= {}
|
|
584
577
|
config.provider[id].options ??= {}
|
|
585
578
|
config.provider[id].options.baseURL = provider.baseURL
|
|
586
|
-
|
|
579
|
+
const apiKey =
|
|
580
|
+
provider.apiKeyEnv === 'PANTHEON_OPENCODE_API_KEY'
|
|
581
|
+
? (env[provider.apiKeyEnv] ?? env.OPENCODE_GO_API_KEY)
|
|
582
|
+
: env[provider.apiKeyEnv]
|
|
583
|
+
config.provider[id].options.apiKey = apiKey
|
|
587
584
|
}
|
|
588
585
|
|
|
589
586
|
for (const [agent, spec] of Object.entries(resolved.agents ?? {})) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool-context types and path guards used by the preserved tool
|
|
3
|
+
* modules (cost-command, goal-loop, model-command, goal-store).
|
|
4
|
+
*
|
|
5
|
+
* These were previously exported from the removed delegation modules; they
|
|
6
|
+
* are hoisted here so the surviving subsystems keep a single source of truth
|
|
7
|
+
* without depending on the deleted delegate machinery.
|
|
8
|
+
*
|
|
9
|
+
* @module tool-context
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The structural tool-execution context a tool's `execute` receives. Mirrors
|
|
14
|
+
* the OpenCode SDK's ToolContext but keeps every field optional so the
|
|
15
|
+
* structural test/embedding surface can be supplied by older hosts.
|
|
16
|
+
*/
|
|
17
|
+
export interface ToolContextLike {
|
|
18
|
+
sessionID: string
|
|
19
|
+
directory?: string
|
|
20
|
+
worktree?: string
|
|
21
|
+
/**
|
|
22
|
+
* The OpenCode SDK's ToolContext declares this as required. It remains
|
|
23
|
+
* optional here because the structural test/embedding surface can be
|
|
24
|
+
* supplied by older hosts; enforcement must skip when it is absent.
|
|
25
|
+
*/
|
|
26
|
+
agent?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Path-traversal guard for session IDs embedded in file paths. Throws on
|
|
31
|
+
* `..`, `/`, `\`. Used by goal-store to keep goal files inside their dir.
|
|
32
|
+
*/
|
|
33
|
+
export function assertSafeParentSessionID(parentSessionID: string): void {
|
|
34
|
+
if (
|
|
35
|
+
parentSessionID.includes('..') ||
|
|
36
|
+
parentSessionID.includes('/') ||
|
|
37
|
+
parentSessionID.includes('\\')
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`Invalid parentSessionID: ${parentSessionID}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
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
3
|
import { BackgroundJobBoard } from './pantheon/background-job-board.ts'
|
|
@@ -12,14 +10,6 @@ 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 {
|
|
16
|
-
collectRootSessionIDs,
|
|
17
|
-
createDelegationTools,
|
|
18
|
-
type DelegationClient,
|
|
19
|
-
resolveDelegationBudgets,
|
|
20
|
-
resolveDelegationTimeoutMs,
|
|
21
|
-
startIdleChildScan,
|
|
22
|
-
} from './pantheon/delegation.ts'
|
|
23
13
|
import { buildCompactionContext } from './pantheon/delegation-compaction.ts'
|
|
24
14
|
import {
|
|
25
15
|
createEnforcementGuard,
|
|
@@ -27,23 +17,16 @@ import {
|
|
|
27
17
|
SessionHierarchyRegistry,
|
|
28
18
|
zeusReadGuard,
|
|
29
19
|
} from './pantheon/delegation-enforce.ts'
|
|
30
|
-
import { handleDelegationEvent } from './pantheon/delegation-notify.ts'
|
|
31
|
-
import { showDelegationTerminalToast, type ToastClient } from './pantheon/delegation-toast.ts'
|
|
32
20
|
import { FilePersistenceAdapter } from './pantheon/file-persistence.ts'
|
|
33
21
|
import { GOAL_LOOP_DEFAULTS, GoalLoop, GoalStore } from './pantheon/goal-loop.ts'
|
|
34
22
|
import { createReadEnhancer } from './pantheon/hashline/read-enhancer.ts'
|
|
35
23
|
import { createHashlineEditTool } from './pantheon/hashline/tool.ts'
|
|
36
24
|
import { createIdleDispatcher } from './pantheon/idle-continuation.ts'
|
|
37
25
|
import { createPantheonLogger } from './pantheon/logger.ts'
|
|
26
|
+
import { createModelCommand } from './pantheon/model-command.ts'
|
|
38
27
|
import { pantheonPluginOnce } from './pantheon/plugin-once.ts'
|
|
39
|
-
import {
|
|
40
|
-
applyActivePresetToConfig,
|
|
41
|
-
loadRoutingAgentModels,
|
|
42
|
-
loadRoutingMaxSteps,
|
|
43
|
-
loadRoutingPermissionTask,
|
|
44
|
-
} from './pantheon/presets.mjs'
|
|
28
|
+
import { applyActivePresetToConfig } from './pantheon/presets.mjs'
|
|
45
29
|
import { safeSessionPath } from './pantheon/session-guard.ts'
|
|
46
|
-
import { StepCapTracker } from './pantheon/step-cap.ts'
|
|
47
30
|
import {
|
|
48
31
|
TODO_ENFORCER_DEFAULTS,
|
|
49
32
|
TodoEnforcer,
|
|
@@ -62,24 +45,12 @@ import { activePresetCandidates, createVisionHandler } from './pantheon/vision.t
|
|
|
62
45
|
// .pantheon/logs/hooks.log; the console echo is opt-in via PANTHEON_HOOKS_LOG=1.
|
|
63
46
|
const log = createPantheonLogger({ module: 'pantheon-plugin' })
|
|
64
47
|
|
|
65
|
-
// R4: per-agent step caps from routing.yml `agents.<name>.max_steps`
|
|
66
|
-
// (fail-open → {} = no agent capped). R1 retry_policy/cooldown are NOT
|
|
67
|
-
// wired here — the plugin has no retry path (opencode cannot intercept task
|
|
68
|
-
// completion); zeus waves load them via presets.mjs loaders and pass them
|
|
69
|
-
// to zeusDelegateWithRetry explicitly.
|
|
70
|
-
const stepCapTracker = new StepCapTracker(loadRoutingMaxSteps({ logger: log }))
|
|
71
|
-
|
|
72
|
-
// O5: permission.task glob rules from routing.yml `permission.task`
|
|
73
|
-
// (fail-open → undefined = existing runtime matrix only).
|
|
74
|
-
const routingPermissionTask = loadRoutingPermissionTask({ logger: log })
|
|
75
|
-
|
|
76
48
|
const board = new BackgroundJobBoard({
|
|
77
49
|
maxConcurrentPerAgent: 3,
|
|
78
50
|
signalDir: '.pantheon/deepwork/board-signals',
|
|
79
51
|
})
|
|
80
52
|
const persistence = new FilePersistenceAdapter('.pantheon/board/state.json')
|
|
81
53
|
board.setPersistence(persistence)
|
|
82
|
-
let terminalToastClient: ToastClient = {}
|
|
83
54
|
board
|
|
84
55
|
.recoverRunningJobs()
|
|
85
56
|
.catch((err) => log.error('[Pantheon Plugin] Failed to recover running jobs:', err))
|
|
@@ -89,17 +60,13 @@ board
|
|
|
89
60
|
// file-only log line (console echo opt-in via PANTHEON_HOOKS_LOG). There is
|
|
90
61
|
// deliberately NO chat delivery — the user policy is zero delegation
|
|
91
62
|
// notifications in the transcript. Completion visibility lives in the board
|
|
92
|
-
// `[unread]` marker
|
|
93
|
-
// TUI toasts (pantheon-hooks, PANTHEON_TOASTS gate) and compaction
|
|
94
|
-
// carry-forward.
|
|
63
|
+
// `[unread]` marker and compaction carry-forward.
|
|
95
64
|
board.onTerminal((taskID: string) => {
|
|
96
65
|
const job = board.get(taskID)
|
|
97
66
|
if (!job) return
|
|
98
67
|
log.info(
|
|
99
68
|
`[Pantheon Plugin] Board terminal: [${job.alias}] ${job.description} → ${job.state}${job.resultSummary ? ` — ${job.resultSummary}` : ''}`,
|
|
100
69
|
)
|
|
101
|
-
// Deliberately direct-to-TUI: onTerminal must not inject chat/transcript text.
|
|
102
|
-
void showDelegationTerminalToast(terminalToastClient, job)
|
|
103
70
|
})
|
|
104
71
|
|
|
105
72
|
// Periodically prune terminal/reconciled jobs (24h TTL, every 30 min).
|
|
@@ -117,10 +84,10 @@ setInterval(
|
|
|
117
84
|
|
|
118
85
|
// ─── Phase 4: Read-Only Enforcement + Compaction Context ───────────────
|
|
119
86
|
|
|
120
|
-
// The read-only registry is populated
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
87
|
+
// The read-only registry is populated when a child session is launched
|
|
88
|
+
// read-only (explicit flag or agent ∈ readOnlyAgents). The guard denies
|
|
89
|
+
// edit/write/bash/task inside those sessions by throwing from
|
|
90
|
+
// tool.execute.before; unknown sessions are allowed (safe default).
|
|
124
91
|
const enforcementGuard = createEnforcementGuard({
|
|
125
92
|
getReadOnlySessions: () => readOnlyRegistry.sessionIDs(),
|
|
126
93
|
options: {
|
|
@@ -141,7 +108,7 @@ const commandNormalizer = createCommandNormalizer({ logger: log })
|
|
|
141
108
|
// Mirrors routing.yml background_delegation.max_compaction_items.
|
|
142
109
|
const COMPACTION_MAX_ITEMS = 10
|
|
143
110
|
|
|
144
|
-
// Root-session registry for the
|
|
111
|
+
// Root-session registry for the child-session depth guard: sessions created
|
|
145
112
|
// without a parentID are roots. Populated from TWO sources:
|
|
146
113
|
// 1. the `session.created` event hook (live roots — fires for sessions
|
|
147
114
|
// created while the plugin runs);
|
|
@@ -149,9 +116,9 @@ const COMPACTION_MAX_ITEMS = 10
|
|
|
149
116
|
// hook (compaction-134) — opencode does NOT replay session.created
|
|
150
117
|
// events for sessions that exist before plugin load, so without the
|
|
151
118
|
// seed a resumed root would never enter this set after a restart.
|
|
152
|
-
//
|
|
153
|
-
// created is a sub-session; unknown sessions are roots") so the
|
|
154
|
-
// works even when this set is incomplete.
|
|
119
|
+
// The hierarchy registry additionally falls back to known children ("any
|
|
120
|
+
// session WE created is a sub-session; unknown sessions are roots") so the
|
|
121
|
+
// depth guard works even when this set is incomplete.
|
|
155
122
|
const rootSessions = new Set<string>()
|
|
156
123
|
const sessionHierarchy = new SessionHierarchyRegistry()
|
|
157
124
|
const sessionAgents = new Map<string, string>()
|
|
@@ -160,14 +127,14 @@ const sessionAgents = new Map<string, string>()
|
|
|
160
127
|
* Seed resumed root sessions without making OpenCode startup depend on the
|
|
161
128
|
* session API responding. Some OpenCode environments do not have the API
|
|
162
129
|
* ready while plugin config hooks run; awaiting session.list() there leaves
|
|
163
|
-
* the TUI stuck before it can open. The
|
|
130
|
+
* the TUI stuck before it can open. The hierarchy registry already treats an
|
|
164
131
|
* unknown session as a root, so this enrichment is deliberately best-effort.
|
|
165
132
|
*/
|
|
166
133
|
function seedRootSessionsInBackground(input: PluginInput): void {
|
|
167
134
|
// Fail-open: environments without the session API (client.session absent or
|
|
168
135
|
// not yet wired while config hooks run) must NOT break startup. `session`
|
|
169
136
|
// is captured and the method invoked on it so `this` stays bound; the
|
|
170
|
-
//
|
|
137
|
+
// hierarchy registry already treats unknown sessions as roots, so skipping
|
|
171
138
|
// the seed is always safe.
|
|
172
139
|
const session = input.client.session
|
|
173
140
|
if (typeof session?.list !== 'function') return
|
|
@@ -177,10 +144,15 @@ function seedRootSessionsInBackground(input: PluginInput): void {
|
|
|
177
144
|
.then((result) => {
|
|
178
145
|
if (!result.error && Array.isArray(result.data)) {
|
|
179
146
|
sessionHierarchy.completeSeed(result.data)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
147
|
+
let seeded = 0
|
|
148
|
+
for (const info of result.data) {
|
|
149
|
+
if (info.parentID === undefined) {
|
|
150
|
+
rootSessions.add(info.id)
|
|
151
|
+
seeded += 1
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (seeded > 0) {
|
|
155
|
+
log.info(`[Pantheon Plugin] Seeded ${seeded} root session(s) from session.list`)
|
|
184
156
|
}
|
|
185
157
|
} else {
|
|
186
158
|
sessionHierarchy.failSeed()
|
|
@@ -198,62 +170,6 @@ function sdkErrorMessage(error: { data?: { message?: string } } | null | undefin
|
|
|
198
170
|
return typeof message === 'string' && message.trim() !== '' ? message : 'request failed'
|
|
199
171
|
}
|
|
200
172
|
|
|
201
|
-
/**
|
|
202
|
-
* Adapt the opencode SDK client to the structural DelegationClient the
|
|
203
|
-
* delegation toolset expects: the SDK wraps every call in a
|
|
204
|
-
* `{data, error, request, response}` result while delegation.ts uses direct
|
|
205
|
-
* returns. Errors surface as throws so the toolset's own error handling
|
|
206
|
-
* (tool errors returned as TEXT) keeps working.
|
|
207
|
-
*/
|
|
208
|
-
function adaptDelegationClient(client: PluginInput['client']): DelegationClient {
|
|
209
|
-
return {
|
|
210
|
-
session: {
|
|
211
|
-
create: async (input) => {
|
|
212
|
-
const body: {
|
|
213
|
-
parentID?: string
|
|
214
|
-
title?: string
|
|
215
|
-
model?: { id: string; providerID: string }
|
|
216
|
-
} = { parentID: input.body.parentID }
|
|
217
|
-
if (input.body.title !== undefined) body.title = input.body.title
|
|
218
|
-
// Forward a model only when the active routing profile supplied one.
|
|
219
|
-
if (input.body.model !== undefined) body.model = input.body.model
|
|
220
|
-
const result = await client.session.create({ body })
|
|
221
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
222
|
-
return { id: result.data.id }
|
|
223
|
-
},
|
|
224
|
-
promptAsync: async (input) => {
|
|
225
|
-
const path = safeSessionPath(input.path.id)
|
|
226
|
-
if (!path) {
|
|
227
|
-
log.warn(`[Pantheon Plugin] promptAsync skipped: invalid sessionID "${input.path.id}"`)
|
|
228
|
-
throw new Error(`invalid sessionID: ${input.path.id}`)
|
|
229
|
-
}
|
|
230
|
-
const result = await client.session.promptAsync({
|
|
231
|
-
path: path.path,
|
|
232
|
-
body: {
|
|
233
|
-
agent: input.body.agent,
|
|
234
|
-
...(input.body.model !== undefined
|
|
235
|
-
? { model: { providerID: input.body.model.providerID, modelID: input.body.model.id } }
|
|
236
|
-
: {}),
|
|
237
|
-
parts: input.body.parts,
|
|
238
|
-
},
|
|
239
|
-
})
|
|
240
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
241
|
-
return result.data
|
|
242
|
-
},
|
|
243
|
-
messages: async (input) => {
|
|
244
|
-
const path = safeSessionPath(input.path.id)
|
|
245
|
-
if (!path) {
|
|
246
|
-
log.warn(`[Pantheon Plugin] messages skipped: invalid sessionID "${input.path.id}"`)
|
|
247
|
-
return []
|
|
248
|
-
}
|
|
249
|
-
const result = await client.session.messages({ path: path.path })
|
|
250
|
-
if (result.error) throw new Error(sdkErrorMessage(result.error))
|
|
251
|
-
return result.data
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
173
|
/**
|
|
258
174
|
* Adapt the opencode SDK client to the structural TodoEnforcerClient the
|
|
259
175
|
* todo enforcer expects: unwraps the SDK `{data, error, request, response}`
|
|
@@ -334,7 +250,6 @@ function adaptTodoEnforcerClient(client: PluginInput['client']): TodoEnforcerCli
|
|
|
334
250
|
* Helpers live in src/pantheon/vision.ts and are imported from there directly.
|
|
335
251
|
*/
|
|
336
252
|
const plugin: Plugin = async (input: PluginInput) => {
|
|
337
|
-
terminalToastClient = input.client as unknown as ToastClient
|
|
338
253
|
// release-134: runtime guard against DOUBLE registration (duplicate toasts +
|
|
339
254
|
// duplicate task_ids in one process — 2026-08-12 live logs). opencode
|
|
340
255
|
// merges the global config (npm-package plugin paths) with the project
|
|
@@ -349,67 +264,12 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
349
264
|
// is older than the installed package. Runs once, fail-open, no auto-sync.
|
|
350
265
|
checkTuiVersionStaleness((msg) => log.warn(msg))
|
|
351
266
|
const vision = createVisionHandler(input)
|
|
352
|
-
// Phase 2/3: background delegation toolset + the bound finalize lifecycle
|
|
353
|
-
// hook. Completion is observed through the event hook below
|
|
354
|
-
// (session.idle / session.error on a child) → finalizeDelegation.
|
|
355
|
-
// Fase B3: runtime controls for the two read-only exceptions and the real
|
|
356
|
-
// per-child wall-clock timer. No task_budget field is injected into the
|
|
357
|
-
// plugin/frontmatter; these are enforced by delegation.ts itself.
|
|
358
|
-
const delegationBudgets = resolveDelegationBudgets()
|
|
359
|
-
const wallClockTimeoutMs = resolveDelegationTimeoutMs()
|
|
360
|
-
|
|
361
|
-
const delegation = createDelegationTools({
|
|
362
|
-
board,
|
|
363
|
-
client: adaptDelegationClient(input.client),
|
|
364
|
-
options: {
|
|
365
|
-
rootSessions,
|
|
366
|
-
isChildSession: (sessionID) => sessionHierarchy.isChild(sessionID),
|
|
367
|
-
registerChildSession: (sessionID, parentID) => {
|
|
368
|
-
sessionHierarchy.register({ id: sessionID, parentID })
|
|
369
|
-
},
|
|
370
|
-
enforceRuntimeMatrix: true,
|
|
371
|
-
readOnlyAgents: new Set(['apollo', 'gaia']),
|
|
372
|
-
// Only an explicitly active routing profile may provide child models.
|
|
373
|
-
// An empty map deliberately lets OpenCode inherit the parent model.
|
|
374
|
-
agentModels: loadRoutingAgentModels({
|
|
375
|
-
candidates: activePresetCandidates(),
|
|
376
|
-
logger: log,
|
|
377
|
-
}),
|
|
378
|
-
delegationBudgets,
|
|
379
|
-
stepCapTracker,
|
|
380
|
-
...(routingPermissionTask != null ? { permissionTask: routingPermissionTask } : {}),
|
|
381
|
-
...(wallClockTimeoutMs !== undefined ? { wallClockTimeoutMs } : {}),
|
|
382
|
-
},
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
// Proactive idle-child scan (Fix 2): finalize children that are idle on the
|
|
386
|
-
// board but have no MD report on disk — eliminates the phantom "running"
|
|
387
|
-
// window. Runs every 30s (configurable via PANTHEON_IDLE_SCAN_INTERVAL_MS),
|
|
388
|
-
// unref'd so it never holds the process open. Fail-open by design.
|
|
389
|
-
const idleScanIntervalMs = Number(process.env.PANTHEON_IDLE_SCAN_INTERVAL_MS) || undefined
|
|
390
|
-
startIdleChildScan(
|
|
391
|
-
{
|
|
392
|
-
board,
|
|
393
|
-
finalize: (childSessionID, opts) => delegation.finalizeDelegation(childSessionID, opts),
|
|
394
|
-
hasReport: (job) => {
|
|
395
|
-
try {
|
|
396
|
-
const filePath = join('.pantheon/delegations', job.parentSessionID, `${job.alias}.md`)
|
|
397
|
-
return existsSync(filePath)
|
|
398
|
-
} catch {
|
|
399
|
-
return false
|
|
400
|
-
}
|
|
401
|
-
},
|
|
402
|
-
logger: log,
|
|
403
|
-
},
|
|
404
|
-
idleScanIntervalMs,
|
|
405
|
-
)
|
|
406
|
-
|
|
407
267
|
// Wave 1 (PR #46): TODO continuation enforcer for root/non-board sessions.
|
|
408
268
|
// Mirrors routing.yml `todo_enforcer` (plugin scope has no routing.yml
|
|
409
|
-
// access — COMPACTION_MAX_ITEMS pattern). The event hook routes
|
|
410
|
-
// session.idle events here
|
|
411
|
-
//
|
|
412
|
-
//
|
|
269
|
+
// access — COMPACTION_MAX_ITEMS pattern). The event hook routes all
|
|
270
|
+
// session.idle events here. Runtime kill-switch: PANTHEON_TODO_ENFORCER=off
|
|
271
|
+
// (routing.yml is a doc mirror only — the env var is the real switch,
|
|
272
|
+
// PANTHEON_TOASTS pattern).
|
|
413
273
|
const todoEnforcer = new TodoEnforcer({
|
|
414
274
|
client: adaptTodoEnforcerClient(input.client),
|
|
415
275
|
board,
|
|
@@ -466,22 +326,23 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
466
326
|
await readEnhancer(input, output)
|
|
467
327
|
}
|
|
468
328
|
|
|
469
|
-
// Wave 4 (PR #46): /cost —
|
|
329
|
+
// Wave 4 (PR #46): /cost — cost + token visibility by agent. Reads
|
|
470
330
|
// opencode.db read-only (node:sqlite, falls back to scripts/cost.mjs).
|
|
471
331
|
// Fully wired (unlike dispatch-guard, which is manual-orchestration-only
|
|
472
332
|
// because opencode 1.18.x cannot intercept task completion via hooks).
|
|
473
333
|
const costCommand = createCostCommand()
|
|
334
|
+
const modelCommand = createModelCommand()
|
|
474
335
|
|
|
475
336
|
return {
|
|
476
337
|
config: async (config: PluginConfig) => {
|
|
477
338
|
// compaction-134 seed: opencode does NOT replay `session.created`
|
|
478
339
|
// events for sessions that exist before plugin load, so after a
|
|
479
340
|
// restart the resumed ROOT session never enters rootSessions and the
|
|
480
|
-
// depth guard would
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
341
|
+
// depth guard would treat it as a child. Seed the registry from
|
|
342
|
+
// session.list() in the background — every session WITHOUT a parentID
|
|
343
|
+
// is a root. Fail-open: a failed/throttled list leaves the set
|
|
344
|
+
// untouched (the hierarchy registry treats unknown sessions as roots),
|
|
345
|
+
// and config setup must never wait for the session API.
|
|
485
346
|
// Context sandbox config — opencode.json → context_sandbox: {enabled, limits}
|
|
486
347
|
try {
|
|
487
348
|
const raw = (config as unknown as Record<string, unknown>).context_sandbox
|
|
@@ -526,19 +387,19 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
526
387
|
log.warn('[plugin] preset application failed (see logs for details)')
|
|
527
388
|
}
|
|
528
389
|
},
|
|
529
|
-
// Phase 2:
|
|
530
|
-
//
|
|
390
|
+
// Phase 2: structural tool registrations (matches the `tool` hook field
|
|
391
|
+
// shape). Native task() owns orchestration; the plugin registers the
|
|
392
|
+
// deterministic /pantheon-model, /cost and full-auto goal tools.
|
|
531
393
|
tool: {
|
|
532
|
-
pantheon_delegate: delegation.pantheon_delegate,
|
|
533
|
-
pantheon_delegation_read: delegation.pantheon_delegation_read,
|
|
534
|
-
pantheon_delegation_list: delegation.pantheon_delegation_list,
|
|
535
394
|
hashline_edit: hashlineEdit,
|
|
536
395
|
// Wave 3 (PR #46): full-auto goal tools (single active goal/session).
|
|
537
396
|
pantheon_goal_create: goalTools.pantheon_goal_create,
|
|
538
397
|
pantheon_goal_update: goalTools.pantheon_goal_update,
|
|
539
398
|
pantheon_goal_get: goalTools.pantheon_goal_get,
|
|
540
|
-
// Wave 4 (PR #46): /cost —
|
|
399
|
+
// Wave 4 (PR #46): /cost — cost report by agent from opencode.db.
|
|
541
400
|
pantheon_cost: costCommand.pantheon_cost,
|
|
401
|
+
// Deterministic /pantheon-model management of top-level model fields.
|
|
402
|
+
pantheon_model: modelCommand.pantheon_model,
|
|
542
403
|
},
|
|
543
404
|
'chat.message': async (hookInput, output) => {
|
|
544
405
|
await vision.chatMessage(hookInput, output)
|
|
@@ -569,22 +430,13 @@ const plugin: Plugin = async (input: PluginInput) => {
|
|
|
569
430
|
if (ev.type === 'session.compacted') {
|
|
570
431
|
await todoPreserver.onCompacted(ev.properties.sessionID)
|
|
571
432
|
}
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
// are
|
|
433
|
+
// Native task() children are owned by OpenCode. Idle routing for
|
|
434
|
+
// generic root/non-board sessions remains priority ordered: an active
|
|
435
|
+
// goal owns the idle (goal loop), otherwise the todo enforcer
|
|
436
|
+
// re-injects. Neither onIdle ever throws (internal failures are logged
|
|
437
|
+
// + swallowed), so the session is safe.
|
|
576
438
|
try {
|
|
577
|
-
|
|
578
|
-
board,
|
|
579
|
-
finalize: (childSessionID, opts) => delegation.finalizeDelegation(childSessionID, opts),
|
|
580
|
-
})
|
|
581
|
-
// Wave 1/3: non-board idle → idle dispatcher. Board children are
|
|
582
|
-
// handled by handleDelegationEvent above; everything else (roots,
|
|
583
|
-
// non-board sessions) is routed by priority: an active goal owns the
|
|
584
|
-
// idle (goal loop), otherwise the todo enforcer re-injects. Neither
|
|
585
|
-
// onIdle ever throws (internal failures are logged + swallowed), so
|
|
586
|
-
// the session is safe.
|
|
587
|
-
if (!delegated && ev.type === 'session.idle') {
|
|
439
|
+
if (ev.type === 'session.idle') {
|
|
588
440
|
await idleDispatcher.onIdle(ev.properties.sessionID)
|
|
589
441
|
}
|
|
590
442
|
} catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"tui.d.ts","names":[],"sources":["../src/index.tsx"],"mappings":";;KAo7BY;;EAEV;;EAEA;;;;EAIA;;EAEA;EACA;;EASA;;EAEA;EACA;EACA;;EAEA;;;;EAIA;;;;;;;;;;;iBAsBc,wBACd,aACA,oBACA,qBACC;;;;;;iBAkHmB,sBAAsB,cAAc,QAAQ;;;;;;;;iBAwC5C,yBAAyB,eAAe,QAAQ;;;;;;;;iBAWtD,sBACd;EAAS;EAAoB;eAC7B;;;;;;iBAYc,YAAY;;;iBAMZ,WAAW;;;;iBAOX,yBAAyB,GAAG,iBAAiB,GAAG;;;;;iBAWhD,sBACd,cAAc,mBACd,sBACA,cACA,4BACC;;cASU;;;;cAKA;;;;;;;;;iBAUG,mBACd,OAAO,iBACP,aACA,uBACC;;;iBAWa,WAAW;;;iBAcX,kBAAkB,OAAO,iBAAiB;;;KAQ9C;iBAQI,mBAAmB,OAAO,kBAAkB;iBAc5C,wBAAwB,OAAO;;iBAoB/B,uBAAuB;;;;;;;iBAWvB,4BACd,mBAAmB,mBACnB,eAAe,wBACd;;KAmFS;EACV;EACA;EACA;EACA;EACA;EACA;IACE;IACA,QAAQ;IACR;IACA;IACA;MAAS;MAAgB;;;;;KAKjB;;EAEV;;EAEA;;EAEA;EACA;;EAEA;EACA;;EAEA;;EAEA;EACA;EACA;EACA;;EAEA;;;KAIU;EACV;EACA;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;iBAac,wBACd,MAAM,oBACN,eACC;;;iBA+Ea,yBACd,KAAK,YAAY,sBACjB,MAAM,oBACN;;;iBA+Fc,sBACd,KAAK,YAAY,sBACjB;;;;;;iBAiBc,2BACd;EAAqB;EAAa;iBAClC,YAAY,uDACX;;;;;iBA2Ba,sBACd,KAAK,YAAY,sBACjB,gBAAgB,sBAChB;;;iBAWc,kBAAkB,MAAM,sBAAsB;;;;;;iBAqB9C,uBACd,eAAe,uBACf,aAAa,oBACZ;;;;;;;;;iBA8Da,iBAAiB,cAAc;;;;KAOnC;;EAEV;EACA;;IAEE;MAAU;;;IAEV;MAAU;QAAY;QAAe,SAAS;;;;;;;;;;iBASlC,wBAAwB,SAAS;;;;;;;;iBAmBjC,gBAAgB;EAAgB;IAAQ;;;;;;;iBASxC,kBAAkB;EAAkC;IAAQ;;;;KAKhE;;EAEV;;EAEA;;;EAGA;;;EAGA;EACA;IACE;IACA;;;;;;;iBAQY,mBAAmB;;;;;iBASnB,cAAc,OAAO;;;;;;;;;;;;;;iBAiBrB,4BACd,mBAAmB,mCACnB,aAAa,mBACb,eACC;;;;;;iBA0Ca,4BACd;EAAS,YAAY,cAAc,SAAS;eAC5C;;;;;KAiLU;EACV,KAAK,YAAY;;EAEjB;;EAEA;;cAucI,QAAQ;EAAoB"}
|
|
@@ -150,7 +150,7 @@ async function resolvePresetForTui(
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/* ─── Version Detection ────────────────────────────────────
|
|
153
|
-
* 100% dinâmico —
|
|
153
|
+
* 100% dinâmico — fallback embutido em build (__PANTHEON_VERSION__).
|
|
154
154
|
* Se falhar, retorna null e a View omite a versão.
|
|
155
155
|
*
|
|
156
156
|
* Try order:
|
|
@@ -159,7 +159,12 @@ async function resolvePresetForTui(
|
|
|
159
159
|
* 1. api.client.file.read package.json
|
|
160
160
|
* 2. git describe --tags
|
|
161
161
|
* 3. opencode --version
|
|
162
|
-
* 4.
|
|
162
|
+
* 4. __PANTHEON_VERSION__ (tsdown define — build-time embed)
|
|
163
|
+
* 5. null */
|
|
164
|
+
|
|
165
|
+
// Build-time embedded version (tsdown `define` in tsdown.config.ts). Injected
|
|
166
|
+
// only into the bundled dist/tui.js; the raw dist/tui.tsx copy falls through.
|
|
167
|
+
declare const __PANTHEON_VERSION__: string | undefined
|
|
163
168
|
|
|
164
169
|
async function detectVersion(api: TuiPluginApi): Promise<string | null> {
|
|
165
170
|
// Try 0a: installed location (2 levels: dist/tui.js → pantheon-tui/package.json)
|
|
@@ -223,6 +228,12 @@ async function detectVersion(api: TuiPluginApi): Promise<string | null> {
|
|
|
223
228
|
/* fall through */
|
|
224
229
|
}
|
|
225
230
|
|
|
231
|
+
// Try 4: build-time embedded version (tsdown define) — last-resort fallback
|
|
232
|
+
// so the sidebar ALWAYS shows a version even when every probe above fails.
|
|
233
|
+
if (typeof __PANTHEON_VERSION__ !== 'undefined' && __PANTHEON_VERSION__) {
|
|
234
|
+
return __PANTHEON_VERSION__
|
|
235
|
+
}
|
|
236
|
+
|
|
226
237
|
return null // sem fallback — versão não aparece
|
|
227
238
|
}
|
|
228
239
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pantheon-tui",
|
|
9
|
-
"version": "1.4.
|
|
9
|
+
"version": "1.4.3",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@opencode-ai/plugin": ">=1.14.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-tui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.3",
|
|
5
5
|
"description": "Pantheon TUI plugin — sidebar (version, preset, sessions, real-time delegations) + AI subscription usage gauges incl. OpenCode Go/Zen dollar limits (vendored, MIT)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|