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.
@@ -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 against
37
- * models.dev api.json on 2026-08-02 (council decision): qwen3.7-max,
38
- * minimax-m2.5 and minimax-m2.7 and glm-5.1/glm-5.2 are TEXT-ONLY (vision:
39
- * false); mimo-v2.5 / mimo-v2.5-free / qwen3.7-plus / minimax-m3 and the
40
- * o-series (o1/o3/o4) are multimodal (models.dev input: text+image).
41
- * claude-* and gpt-5.6-* carry native image support.
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-02).
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
- // opencode-go (OpenCode Go subscription) — bare segment prefixes so
69
- // opencode-go/<model> IDs match; the provider-scoped deepseek/ entries
70
- // above still win by length for deepseek/ models. glm-5.1/glm-5.2 are
71
- // text-only per models.dev api.json (2026-08-02).
72
- { prefix: 'glm-5.2', maxEffort: 'medium', stripEffort: false, vision: false },
73
- { prefix: 'glm-5.1', maxEffort: 'medium', stripEffort: false, vision: false },
74
- { prefix: 'kimi-k2.6', maxEffort: 'high', stripEffort: false, vision: true },
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-ultra-free / north-mini-code-free verified
98
- // text-only via models.dev (2026-08-02).
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 key = env[envVar]
517
- return typeof key === 'string' && key.trim() !== ''
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
- config.provider[id].options.apiKey = env[provider.apiKeyEnv]
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 (pantheon_delegation_list), pantheon_delegation_read,
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 by createDelegationTools (delegation.ts)
121
- // when a delegate is read-only (explicit flag or agent ∈ readOnlyAgents).
122
- // The guard denies edit/write/bash/task inside those sessions by throwing
123
- // from tool.execute.before; unknown sessions are allowed (safe default).
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 delegation depth guard: sessions created
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
- // delegation.ts additionally falls back to knownChildren ("any session WE
153
- // created is a sub-session; unknown sessions are roots") so the depth guard
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 delegation layer already treats an
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
- // delegation layer already treats unknown sessions as roots, so skipping
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
- const seeded = collectRootSessionIDs(result.data)
181
- if (seeded.size > 0) {
182
- for (const id of seeded) rootSessions.add(id)
183
- log.info(`[Pantheon Plugin] Seeded ${seeded.size} root session(s) from session.list`)
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 non-board
410
- // session.idle events here; board-child idles go to finalizeDelegation.
411
- // Runtime kill-switch: PANTHEON_TODO_ENFORCER=off (routing.yml is a doc
412
- // mirror only — the env var is the real switch, PANTHEON_TOASTS pattern).
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 — delegation cost + token visibility. Reads
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 reject its pantheon_delegate calls. Seed the
481
- // registry from session.list() in the background — every session
482
- // WITHOUT a parentID is a root. Fail-open: a failed/throttled list
483
- // leaves the set untouched (delegation.ts treats unknown sessions as
484
- // roots), and config setup must never wait for the session API.
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: background delegation tools (structural — matches the `tool`
530
- // hook field shape; read_only sessions feed the Phase 4 registry).
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 — delegation cost report from opencode.db.
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
- // Phase 3: observe completion on child sessions → finalizeDelegation.
573
- // The board transition fires onTerminal → the file-only audit log (no
574
- // chat delivery — see the onTerminal listener above). Unknown sessions
575
- // are a no-op.
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
- const delegated = await handleDelegationEvent(ev, {
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":";;KAy6BY;;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"}
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"}
@@ -164,7 +164,7 @@ async function detectVersion(api) {
164
164
  if (ver) return ver;
165
165
  }
166
166
  } catch {}
167
- return null;
167
+ return "1.4.2";
168
168
  }
169
169
  /**
170
170
  * usage-bar — AI subscription usage gauge for the opencode TUI.
@@ -150,7 +150,7 @@ async function resolvePresetForTui(
150
150
  }
151
151
 
152
152
  /* ─── Version Detection ────────────────────────────────────
153
- * 100% dinâmico — zero fallback hardcoded.
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. null */
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.1",
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.1",
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.1",
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": {