pantheon-opencode 1.6.0-beta.1 → 1.6.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/AGENTS.global.md +206 -0
  2. package/AGENTS.md +10 -891
  3. package/CHANGELOG.md +350 -0
  4. package/README.md +561 -52
  5. package/README.pt-BR.md +285 -20
  6. package/bin/pantheon-init.mjs +70 -4
  7. package/commands/pantheon-audit.md +7 -1
  8. package/commands/pantheon-consolidate.md +1 -1
  9. package/commands/pantheon-deepwork.md +14 -3
  10. package/commands/pantheon-focus.md +17 -0
  11. package/commands/pantheon-vision.md +61 -12
  12. package/package.json +24 -17
  13. package/plugin.json +2 -2
  14. package/pyproject.toml +8 -6
  15. package/scripts/build-agents-md.mjs +428 -56
  16. package/scripts/changelog-merge.mjs +161 -0
  17. package/scripts/{code_mode_server.py → code_mode.py} +3 -3
  18. package/scripts/cost.mjs +244 -41
  19. package/scripts/doctor.mjs +151 -1
  20. package/scripts/install/agents-md.mjs +4 -4
  21. package/scripts/install/backup.mjs +475 -0
  22. package/scripts/install/cli-ui.mjs +6 -1
  23. package/scripts/install/config-migration.mjs +317 -4
  24. package/scripts/install/exit-codes.mjs +26 -0
  25. package/scripts/install/health-check.mjs +21 -19
  26. package/scripts/install/interactive.mjs +16 -2
  27. package/scripts/install/migrate.mjs +41 -22
  28. package/scripts/install/opencode-version.mjs +246 -4
  29. package/scripts/install/opencode.mjs +1175 -215
  30. package/scripts/install/plan.mjs +287 -0
  31. package/scripts/install/plugin.mjs +62 -14
  32. package/scripts/install/shared.mjs +190 -48
  33. package/scripts/install/strings.mjs +20 -0
  34. package/scripts/install/venv.mjs +13 -3
  35. package/scripts/install/vision-prompt.mjs +171 -0
  36. package/scripts/install-mcp.mjs +28 -9
  37. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
  38. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
  39. package/scripts/{memory_mcp_server.py → memory_mcp.py} +2 -2
  40. package/scripts/probe-context-rehydrate.mjs +8 -2
  41. package/scripts/probe-pantheon-cost.mjs +71 -15
  42. package/scripts/prune.mjs +9 -3
  43. package/scripts/restore.mjs +222 -0
  44. package/scripts/run-node-tests.mjs +189 -0
  45. package/scripts/sync-artifacts.mjs +26 -13
  46. package/scripts/sync-tui.mjs +24 -3
  47. package/scripts/test-opencode-v2-sandbox.sh +180 -26
  48. package/scripts/validate-routing.mjs +3 -3
  49. package/scripts/validate-sandbox-install.mjs +138 -0
  50. package/scripts/versioning.mjs +13 -1
  51. package/scripts/vision-status.mjs +682 -0
  52. package/skills-lock.json +8 -8
  53. package/src/agents/aphrodite.md +20 -37
  54. package/src/agents/apollo.md +7 -3
  55. package/src/agents/athena.md +63 -35
  56. package/src/agents/demeter.md +8 -28
  57. package/src/agents/gaia.md +1 -1
  58. package/src/agents/hephaestus.md +10 -21
  59. package/src/agents/hermes.md +15 -38
  60. package/src/agents/iris.md +7 -1
  61. package/src/agents/mnemosyne.md +18 -54
  62. package/src/agents/nyx.md +1 -1
  63. package/src/agents/prometheus.md +16 -20
  64. package/src/agents/talos.md +10 -2
  65. package/src/agents/themis.md +5 -5
  66. package/src/agents/zeus.md +99 -172
  67. package/src/instructions/agent-return-format.instructions.md +34 -17
  68. package/src/instructions/backend-standards.instructions.md +2 -0
  69. package/src/instructions/frontend-standards.instructions.md +3 -1
  70. package/src/instructions/memory-protocol.instructions.md +12 -110
  71. package/src/instructions/test-economy.instructions.md +21 -0
  72. package/src/instructions/zeus-anti-stall.instructions.md +20 -67
  73. package/src/instructions/zeus-communication-rules.instructions.md +0 -1
  74. package/src/instructions/zeus-council-synthesis.instructions.md +31 -260
  75. package/src/instructions/zeus-memory-operations.instructions.md +30 -0
  76. package/src/instructions/zeus-timeout-retry.instructions.md +61 -89
  77. package/src/mcp/{code_mode_server.py → code_mode.py} +3 -3
  78. package/src/mcp/eval_store.py +2 -2
  79. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +52 -17
  80. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
  81. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +2 -2
  82. package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +286 -42
  83. package/src/mcp/requirements-mcp.txt +6 -4
  84. package/src/mcp/requirements-vision.txt +2 -2
  85. package/src/pantheon/cost-command.ts +403 -102
  86. package/src/pantheon/delegation-enforce.ts +10 -1
  87. package/src/pantheon/model-agents.ts +19 -0
  88. package/src/pantheon/model-command.ts +218 -56
  89. package/src/pantheon/presets.d.mts +0 -8
  90. package/src/pantheon/presets.mjs +5 -32
  91. package/src/pantheon/v2-bounded-read.ts +640 -0
  92. package/src/pantheon/v2-delegation-enforce.ts +214 -0
  93. package/src/pantheon/v2-execute-after.ts +186 -0
  94. package/src/pantheon/v2-model-overrides.ts +275 -0
  95. package/src/pantheon/v2-tools.ts +418 -0
  96. package/src/pantheon/v2-unsupported.d.mts +11 -0
  97. package/src/pantheon/v2-unsupported.mjs +90 -0
  98. package/src/pantheon/v2-vision.ts +622 -0
  99. package/src/pantheon/vision-local.ts +531 -0
  100. package/src/pantheon/vision.ts +1160 -115
  101. package/src/plugin-v2.ts +1158 -219
  102. package/src/plugin.ts +12 -9
  103. package/src/plugins/tui/dist/server.d.ts +1 -2
  104. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  105. package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
  106. package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
  107. package/src/plugins/tui/dist/tui-v2.js +659 -0
  108. package/src/plugins/tui/dist/tui-v2.js.map +1 -0
  109. package/src/plugins/tui/dist/tui.d.ts +85 -82
  110. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  111. package/src/plugins/tui/dist/tui.js +72 -59
  112. package/src/plugins/tui/index-v2.ts +6 -0
  113. package/src/plugins/tui/package-lock.json +5516 -1353
  114. package/src/plugins/tui/package.json +22 -12
  115. package/src/plugins/tui/src/index-v2.tsx +734 -0
  116. package/src/plugins/tui/src/index.tsx +106 -85
  117. package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
  118. package/src/plugins/tui/tsconfig.build.json +5 -0
  119. package/src/plugins/tui/tsconfig.typecheck.json +5 -0
  120. package/src/plugins/tui/tsdown.config.ts +22 -2
  121. package/src/plugins/tui/tui-v2.ts +2 -0
  122. package/src/plugins/tui/tui.ts +3 -0
  123. package/src/routing.yml +11 -21
  124. package/src/skills/README.md +3 -3
  125. package/src/skills/agent-coordination/SKILL.md +7 -2
  126. package/src/skills/artifact-management/SKILL.md +12 -5
  127. package/src/skills/auto-continue/SKILL.md +52 -35
  128. package/src/skills/code-review-checklist/SKILL.md +5 -5
  129. package/src/skills/context-compression/SKILL.md +26 -879
  130. package/src/skills/session-goal/SKILL.md +14 -5
  131. package/src/skills/spec-driven-development/SKILL.md +3 -1
  132. package/src/skills/tdd-with-agents/SKILL.md +18 -10
  133. package/opencode.json +0 -58
@@ -9,12 +9,14 @@
9
9
  * module never needs the opencode SDK — the handler is a structural closure
10
10
  * wired from plugin.ts.
11
11
  *
12
- * Blocked tools (edit, write, bash, task, hashline_edit):
12
+ * Blocked tools (edit, write, bash, task, hashline_edit, pantheon_model):
13
13
  * - edit/write/bash are the mutating surface — read-only agents (apollo,
14
14
  * gaia) must stay investigation-only.
15
15
  * - `hashline_edit` is the Wave 2 (PR #46) tag-anchored edit tool — it is a
16
16
  * write surface too, so read-only agents must not bypass enforcement via
17
17
  * it.
18
+ * - `pantheon_model` writes active-preset.json in project and global scope —
19
+ * a config write a read-only session must not perform.
18
20
  * - `task` is blocked too, which hard-enforces depth-2: a read-only agent
19
21
  * cannot spawn its own subagents.
20
22
  *
@@ -182,6 +184,13 @@ export const DEFAULT_BLOCKED_TOOLS: ReadonlySet<string> = new Set([
182
184
  'bash',
183
185
  'task',
184
186
  'hashline_edit',
187
+ // `pantheon_model` writes active-preset.json in BOTH the project and the
188
+ // global scope, so it is a write surface with the same blast radius as
189
+ // `write`. Read-only agents have no reason to change agent models, and a
190
+ // global override from an investigation-only session is unrecoverable from
191
+ // inside that session. `pantheon_cost` is deliberately absent: it only reads
192
+ // opencode.db.
193
+ 'pantheon_model',
185
194
  ])
186
195
 
187
196
  // ─── Zeus Read Guard ──────────────────────────────────────────────────
@@ -0,0 +1,19 @@
1
+ /** Canonical agent IDs shared by model routing and V2 override validation. */
2
+ export const KNOWN_AGENTS = [
3
+ 'zeus',
4
+ 'athena',
5
+ 'apollo',
6
+ 'hermes',
7
+ 'aphrodite',
8
+ 'demeter',
9
+ 'themis',
10
+ 'prometheus',
11
+ 'hephaestus',
12
+ 'nyx',
13
+ 'gaia',
14
+ 'iris',
15
+ 'mnemosyne',
16
+ 'talos',
17
+ ] as const
18
+
19
+ export type KnownAgent = (typeof KNOWN_AGENTS)[number]
@@ -8,7 +8,7 @@
8
8
  * - set --agent X --model Y --effort Z --scope project|global: validates
9
9
  * via CAPABILITY_TABLE/hasVision and persists atomically with .bak + lock.
10
10
  * - reset --agent X [--scope]: removes override.
11
- * - wizard (no args): askQuestions flow agent → model → effort → scope.
11
+ * - legacy V1 wizard (no args): askQuestions flow agent → model → effort → scope.
12
12
  *
13
13
  * Atomic writes use sibling .bak and temporary-file rename with O_NOFOLLOW
14
14
  * and per-path in-memory lock.
@@ -23,6 +23,7 @@ import { lstat, mkdir, open, rename, unlink } from 'node:fs/promises'
23
23
  import { homedir } from 'node:os'
24
24
  import { basename, dirname, join, resolve } from 'node:path'
25
25
  import { z } from 'zod'
26
+ import { KNOWN_AGENTS } from './model-agents.ts'
26
27
  import {
27
28
  capabilityEntry,
28
29
  hasVision,
@@ -31,6 +32,10 @@ import {
31
32
  resolveActivePreset,
32
33
  } from './presets.mjs'
33
34
  import type { ToolContextLike } from './tool-context.ts'
35
+ import { readV2ModelConfiguration } from './v2-model-overrides.ts'
36
+
37
+ export type { KnownAgent } from './model-agents.ts'
38
+ export { KNOWN_AGENTS } from './model-agents.ts'
34
39
 
35
40
  export type ModelScope = 'project' | 'global'
36
41
  export type ModelAction = 'status' | 'show' | 'set' | 'reset'
@@ -38,24 +43,6 @@ export type ModelAction = 'status' | 'show' | 'set' | 'reset'
38
43
  const MODEL_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._:+-]*$/
39
44
  const SAFE_PRESET_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
40
45
 
41
- /** 14 canonical agents — must stay in sync with src/routing.yml agents. */
42
- export const KNOWN_AGENTS = [
43
- 'zeus',
44
- 'athena',
45
- 'apollo',
46
- 'hermes',
47
- 'aphrodite',
48
- 'demeter',
49
- 'themis',
50
- 'prometheus',
51
- 'hephaestus',
52
- 'nyx',
53
- 'gaia',
54
- 'iris',
55
- 'mnemosyne',
56
- 'talos',
57
- ] as const
58
- export type KnownAgent = (typeof KNOWN_AGENTS)[number]
59
46
  const KNOWN_AGENTS_SET = new Set<string>(KNOWN_AGENTS)
60
47
 
61
48
  const modelArgs = {
@@ -107,6 +94,10 @@ export interface ModelCommandOptions {
107
94
  ask?: (questions: unknown[]) => Promise<Record<string, unknown>>
108
95
  /** Optional readline injection for wizard fallback. */
109
96
  rl?: { question: (q: string) => Promise<string>; close?: () => void }
97
+ /** V2 ignores static presets and reports/applies only explicit per-agent overrides. */
98
+ v2Mode?: boolean
99
+ /** Filesystem seam for secure V2 preset reads, including post-write verification. */
100
+ v2PresetReadFileSystem?: V2PresetReadFileSystem
110
101
  }
111
102
 
112
103
  export interface ModelTool {
@@ -127,6 +118,22 @@ interface ReadFileResult {
127
118
  data: unknown
128
119
  }
129
120
 
121
+ interface V2PresetReadStats {
122
+ isFile(): boolean
123
+ mode: number
124
+ }
125
+
126
+ interface V2PresetReadHandle {
127
+ stat(): Promise<V2PresetReadStats>
128
+ readFile(encoding: 'utf8'): Promise<string>
129
+ close(): Promise<void>
130
+ }
131
+
132
+ /** Filesystem operations injectable for focused V2 race-safety tests. */
133
+ export interface V2PresetReadFileSystem {
134
+ open(path: string, flags: number): Promise<V2PresetReadHandle>
135
+ }
136
+
130
137
  interface AgentStatus {
131
138
  agent: string
132
139
  model: string | undefined
@@ -375,6 +382,75 @@ async function readActivePresetRaw(path: string, label: string): Promise<ReadFil
375
382
  }
376
383
  }
377
384
 
385
+ /** Read V2 mutation state from one nonblocking, no-follow file descriptor. */
386
+ export async function readV2ActivePresetRaw(
387
+ path: string,
388
+ label: string,
389
+ fileSystem: V2PresetReadFileSystem = {
390
+ open: async (filePath, flags) => open(filePath, flags),
391
+ },
392
+ ): Promise<ReadFileResult> {
393
+ const noFollowFlag = constants.O_NOFOLLOW
394
+ const nonBlockFlag = constants.O_NONBLOCK
395
+ if (
396
+ noFollowFlag === undefined ||
397
+ noFollowFlag === 0 ||
398
+ nonBlockFlag === undefined ||
399
+ nonBlockFlag === 0
400
+ ) {
401
+ throw new ModelCommandError(
402
+ `${label} cannot be read securely on this platform; configuration was not changed`,
403
+ )
404
+ }
405
+
406
+ let file: V2PresetReadHandle | undefined
407
+ try {
408
+ file = await fileSystem.open(path, constants.O_RDONLY | noFollowFlag | nonBlockFlag)
409
+ const openedStats = await file.stat()
410
+ if (!openedStats.isFile()) {
411
+ throw new ModelCommandError(
412
+ `${label} must be a regular file and must not be a symlink; configuration was not changed`,
413
+ )
414
+ }
415
+ const content = await file.readFile('utf8')
416
+ try {
417
+ const parsed: unknown = JSON.parse(content)
418
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
419
+ return {
420
+ exists: true,
421
+ valid: false,
422
+ data: null,
423
+ content,
424
+ mode: openedStats.mode & PERMISSION_MASK,
425
+ }
426
+ }
427
+ return {
428
+ exists: true,
429
+ valid: true,
430
+ data: parsed as Record<string, unknown>,
431
+ content,
432
+ mode: openedStats.mode & PERMISSION_MASK,
433
+ }
434
+ } catch {
435
+ return {
436
+ exists: true,
437
+ valid: false,
438
+ data: null,
439
+ content,
440
+ mode: openedStats.mode & PERMISSION_MASK,
441
+ }
442
+ }
443
+ } catch (error: unknown) {
444
+ if (errorCode(error) === 'ENOENT') return { exists: false, valid: true, data: null }
445
+ if (errorCode(error) === 'ELOOP') {
446
+ throw new ModelCommandError(`${label} must not be a symlink; configuration was not changed`)
447
+ }
448
+ throw error
449
+ } finally {
450
+ await file?.close().catch(() => undefined)
451
+ }
452
+ }
453
+
378
454
  function normalizeAgentName(value: string): string {
379
455
  return value.trim().toLowerCase()
380
456
  }
@@ -464,11 +540,14 @@ async function writeActivePresetAtomically(
464
540
 
465
541
  function ensureActivePresetShape(
466
542
  raw: Record<string, unknown> | null,
467
- fallbackPreset: string,
543
+ fallbackPreset?: string,
468
544
  ): Record<string, unknown> {
469
545
  const out: Record<string, unknown> = raw && typeof raw === 'object' ? { ...raw } : {}
470
546
  if (typeof out.version !== 'number') out.version = 1
471
- if (typeof out.preset !== 'string' || (out.preset as string).trim() === '') {
547
+ if (
548
+ fallbackPreset !== undefined &&
549
+ (typeof out.preset !== 'string' || (out.preset as string).trim() === '')
550
+ ) {
472
551
  out.preset = fallbackPreset
473
552
  }
474
553
  if (typeof out.source !== 'string') out.source = 'cli'
@@ -516,34 +595,39 @@ async function updateActivePreset(
516
595
 
517
596
  const path = presetPathForScope(options, scope)
518
597
  return withPathLock(path, async () => {
519
- const current = await readActivePresetRaw(path, 'configuration file')
598
+ const current = options.v2Mode
599
+ ? await readV2ActivePresetRaw(path, 'configuration file', options.v2PresetReadFileSystem)
600
+ : await readActivePresetRaw(path, 'configuration file')
520
601
  if (current.exists && !current.valid) {
521
602
  throw new ModelCommandError(`${scope} preset file contains invalid JSON; file preserved`)
522
603
  }
523
604
 
524
- const env = envValue(options)
525
- let fallbackPreset = defaultFallbackPreset()
526
- // Prefer env preset if set and known
527
- const envPreset = env.PANTHEON_MODEL_PRESET?.trim()
528
- if (envPreset && envPreset !== '' && envPreset !== 'none') {
529
- try {
530
- const defs = loadPresetDefs()
531
- if (Object.hasOwn(defs, envPreset)) fallbackPreset = envPreset
532
- } catch {
533
- // ignore
534
- }
535
- } else if (
536
- current.data &&
537
- typeof current.data === 'object' &&
538
- (current.data as Record<string, unknown>).preset
539
- ) {
540
- const existing = (current.data as Record<string, unknown>).preset as string
541
- if (
542
- typeof existing === 'string' &&
543
- existing.trim() !== '' &&
544
- SAFE_PRESET_NAME.test(existing.trim())
605
+ let fallbackPreset: string | undefined
606
+ if (!options.v2Mode) {
607
+ const env = envValue(options)
608
+ fallbackPreset = defaultFallbackPreset()
609
+ // Prefer env preset if set and known (V1 legacy behavior only).
610
+ const envPreset = env.PANTHEON_MODEL_PRESET?.trim()
611
+ if (envPreset && envPreset !== '' && envPreset !== 'none') {
612
+ try {
613
+ const defs = loadPresetDefs()
614
+ if (Object.hasOwn(defs, envPreset)) fallbackPreset = envPreset
615
+ } catch {
616
+ // ignore
617
+ }
618
+ } else if (
619
+ current.data &&
620
+ typeof current.data === 'object' &&
621
+ (current.data as Record<string, unknown>).preset
545
622
  ) {
546
- fallbackPreset = existing.trim()
623
+ const existing = (current.data as Record<string, unknown>).preset as string
624
+ if (
625
+ typeof existing === 'string' &&
626
+ existing.trim() !== '' &&
627
+ SAFE_PRESET_NAME.test(existing.trim())
628
+ ) {
629
+ fallbackPreset = existing.trim()
630
+ }
547
631
  }
548
632
  }
549
633
 
@@ -603,13 +687,25 @@ async function updateActivePreset(
603
687
 
604
688
  // health-check: verify file parses and preset resolves
605
689
  try {
606
- const written = await readRegularFile(path, 'configuration file')
607
- if (written) JSON.parse(written.content)
690
+ if (options.v2Mode) {
691
+ const written = await readV2ActivePresetRaw(
692
+ path,
693
+ 'configuration file',
694
+ options.v2PresetReadFileSystem,
695
+ )
696
+ if (written.exists && written.content !== undefined) JSON.parse(written.content)
697
+ } else {
698
+ const written = await readRegularFile(path, 'configuration file')
699
+ if (written) JSON.parse(written.content)
700
+ }
608
701
  } catch {
609
702
  options.logger?.warn?.('health-check: active-preset.json verification failed')
610
703
  }
611
704
 
612
- const base = `Model override set (${scope}): ${agentKey} → ${model} (effort: ${variantInfo.variant ?? 'none'}). Restart OpenCode for the change to take effect.`
705
+ const activation = options.v2Mode
706
+ ? 'V2 reads this override during the next agent-config transform on a subsequent OpenCode configuration load; the current session is unchanged.'
707
+ : 'Restart OpenCode for the change to take effect.'
708
+ const base = `Model override set (${scope}): ${agentKey} → ${model} (effort: ${variantInfo.variant ?? 'none'}). ${activation}`
613
709
  return visionWarning ? `${base}\n${visionWarning}` : base
614
710
  } else {
615
711
  // reset
@@ -620,7 +716,10 @@ async function updateActivePreset(
620
716
  // Clean up empty overrides.agents to keep file tidy, but keep overrides object
621
717
  // Remove empty overrides if no agents and no providers etc? Keep minimal.
622
718
  await writeActivePresetAtomically(path, shaped, current)
623
- return `Model override removed (${scope}): ${agentKey}. Restart OpenCode for the change to take effect.`
719
+ const activation = options.v2Mode
720
+ ? 'V2 reads overrides during the next agent-config transform on a subsequent OpenCode configuration load; the current session is unchanged.'
721
+ : 'Restart OpenCode for the change to take effect.'
722
+ return `Model override removed (${scope}): ${agentKey}. ${activation}`
624
723
  }
625
724
  })
626
725
  }
@@ -630,6 +729,7 @@ async function updateActivePreset(
630
729
  async function resolvePerAgentStatus(options: ModelCommandOptions): Promise<{
631
730
  presetName: string | null
632
731
  presetSource: string | null
732
+ legacyPresetMarker: { source: 'env' | 'file' } | null
633
733
  agents: AgentStatus[]
634
734
  warnings: string[]
635
735
  }> {
@@ -637,6 +737,36 @@ async function resolvePerAgentStatus(options: ModelCommandOptions): Promise<{
637
737
  const candidates = activePresetCandidatesList(options)
638
738
  const warnings: string[] = []
639
739
 
740
+ if (options.v2Mode) {
741
+ const configuration = readV2ModelConfiguration({ candidates, env })
742
+ warnings.push(...configuration.warnings)
743
+ const agents = KNOWN_AGENTS.map((agent): AgentStatus => {
744
+ const override = configuration.overrides[agent]
745
+ let vision: boolean | undefined
746
+ if (override?.model) {
747
+ try {
748
+ vision = hasVision(override.model)
749
+ } catch {
750
+ vision = undefined
751
+ }
752
+ }
753
+ return {
754
+ agent,
755
+ model: override?.model,
756
+ effort: override?.variant,
757
+ origin: override ? 'override' : 'none',
758
+ vision,
759
+ }
760
+ })
761
+ return {
762
+ presetName: null,
763
+ presetSource: null,
764
+ legacyPresetMarker: configuration.legacyPresetMarker,
765
+ agents,
766
+ warnings,
767
+ }
768
+ }
769
+
640
770
  // Use presets.mjs resolver (sync) for canonical resolution, but capture warnings
641
771
  const logger: ModelCommandLogger = {
642
772
  warn: (m) => warnings.push(m),
@@ -702,16 +832,37 @@ async function resolvePerAgentStatus(options: ModelCommandOptions): Promise<{
702
832
  return {
703
833
  presetName: (resolved as { name?: string } | null)?.name ?? null,
704
834
  presetSource: (resolved as { source?: string } | null)?.source ?? null,
835
+ legacyPresetMarker: null,
705
836
  agents,
706
837
  warnings,
707
838
  }
708
839
  }
709
840
 
710
841
  async function renderStatus(options: ModelCommandOptions): Promise<string> {
711
- const { presetName, presetSource, agents, warnings } = await resolvePerAgentStatus(options)
842
+ const { presetName, presetSource, legacyPresetMarker, agents, warnings } =
843
+ await resolvePerAgentStatus(options)
712
844
  const lines: string[] = []
713
- lines.push('Pantheon model configuration per-agent (restart OpenCode after changes):')
714
- if (presetName) {
845
+ lines.push(
846
+ options.v2Mode
847
+ ? 'Pantheon model configuration per-agent (configured values only; not a readout of the model serving a completion):'
848
+ : 'Pantheon model configuration per-agent (restart OpenCode after changes):',
849
+ )
850
+ if (options.v2Mode) {
851
+ lines.push('V2 manual override scope: project only; global overrides are not read or applied.')
852
+ if (legacyPresetMarker) {
853
+ const markerSource =
854
+ legacyPresetMarker.source === 'env' ? 'PANTHEON_MODEL_PRESET' : 'active-preset.json'
855
+ lines.push(
856
+ `Legacy preset marker present in ${markerSource} — ignored in V2; static presets are retired.`,
857
+ )
858
+ } else {
859
+ lines.push('Legacy preset marker: none (static presets are retired and ignored in V2).')
860
+ }
861
+ lines.push('Host default remains unchanged when no manual override is set.')
862
+ lines.push(
863
+ 'Manual project-scoped per-agent overrides are read by the V2 agent transform on the next OpenCode configuration load; the current session is unchanged.',
864
+ )
865
+ } else if (presetName) {
715
866
  const originLabel =
716
867
  presetSource === 'env' ? 'env' : presetSource === 'file' ? 'preset' : presetSource
717
868
  lines.push(`active preset: ${presetName} (${presetSource ?? 'unknown'})`)
@@ -736,7 +887,7 @@ async function renderStatus(options: ModelCommandOptions): Promise<string> {
736
887
  return lines.join('\n')
737
888
  }
738
889
 
739
- // ─── Wizard (askQuestions) ──────────────────────────────────────────────────
890
+ // ─── Legacy V1 wizard (askQuestions) ────────────────────────────────────────
740
891
 
741
892
  export function buildWizardQuestions(): unknown[] {
742
893
  return [
@@ -875,13 +1026,23 @@ async function runWizard(options: ModelCommandOptions): Promise<string> {
875
1026
  }
876
1027
  }
877
1028
 
878
- /** Create the deterministic `pantheon_model` tool used by `/pantheon-model`. */
1029
+ /** Create the `pantheon_model` tool; the interactive wizard is V1-only. */
879
1030
  export function createModelCommand(options: ModelCommandOptions = {}): ModelToolset {
1031
+ const args = options.v2Mode
1032
+ ? {
1033
+ ...modelArgs,
1034
+ action: modelArgs.action.describe(
1035
+ 'Operation: status/show, set, or reset. V1 no-args invocation runs the legacy wizard.',
1036
+ ),
1037
+ }
1038
+ : modelArgs
1039
+
880
1040
  return {
881
1041
  pantheon_model: {
882
- description:
883
- 'Show, set, or reset per-agent model overrides in active-preset.json (project or global). Overrides win over preset; env preset wins over file. Never writes .env or top-level model/small_model.',
884
- args: modelArgs,
1042
+ description: options.v2Mode
1043
+ ? 'Show, set, or reset explicit per-agent project-scoped model overrides in active-preset.json. Global overrides are not read or written in V2; future support requires separate explicit authorization. V2 ignores static preset names and PANTHEON_MODEL_PRESET; no wizard or implicit model selection. Status reports project-scoped configured values only and never writes .env or top-level model/small_model.'
1044
+ : 'Show, set, or reset per-agent model overrides in active-preset.json (project or global). Overrides win over preset; env preset wins over file. Never writes .env or top-level model/small_model.',
1045
+ args,
885
1046
  execute: async (args, _ctx) => {
886
1047
  try {
887
1048
  const hasAction = args.action !== undefined
@@ -890,6 +1051,7 @@ export function createModelCommand(options: ModelCommandOptions = {}): ModelTool
890
1051
 
891
1052
  // Wizard when invoked with no args at all
892
1053
  if (!hasAction && !hasAgentModelEffort) {
1054
+ if (options.v2Mode) return await renderStatus(options)
893
1055
  return await runWizard(options)
894
1056
  }
895
1057
 
@@ -104,14 +104,6 @@ export interface RoutingRetryPolicyOptions {
104
104
  logger?: { warn?: (msg: string) => void }
105
105
  }
106
106
 
107
- /**
108
- * R4: per-agent step caps from routing.yml `agents.<name>.max_steps`.
109
- * Fail-open → {} (no agent capped).
110
- */
111
- export declare function loadRoutingMaxSteps(
112
- options?: RoutingRetryPolicyOptions,
113
- ): Record<string, number>
114
-
115
107
  /**
116
108
  * O5: permission.task glob rules from routing.yml `permission.task`
117
109
  * (glob pattern → 'allow' | 'deny'). Fail-open → null.
@@ -26,7 +26,7 @@
26
26
  import { readFileSync } from 'node:fs'
27
27
  import { join } from 'node:path'
28
28
  import { fileURLToPath } from 'node:url'
29
- import yaml from 'js-yaml'
29
+ import { load } from 'js-yaml'
30
30
 
31
31
  export const EFFORT_RANK = { low: 0, medium: 1, high: 2 }
32
32
 
@@ -61,6 +61,8 @@ export const CAPABILITY_TABLE = [
61
61
  // entry above matches 'mimo/v2.5' (full-string) but NOT 'mimo-v2.5'
62
62
  // (segment 'mimo-v2.5' has no slash, so 'mimo/' prefix cannot match).
63
63
  { prefix: 'mimo-v2.5', maxEffort: 'low', stripEffort: false, vision: true },
64
+ // Current free OpenCode Zen vision model (renamed to the v2.6 Flash family).
65
+ { prefix: 'mimo-v2.6-flash', maxEffort: 'low', stripEffort: false, vision: true },
64
66
  // mimo-v2.5-pro is text-only per models.dev api.json (2026-08-27).
65
67
  { prefix: 'mimo-v2.5-pro', maxEffort: 'low', stripEffort: false, vision: false },
66
68
  // Go subscription specific — bare segment prefixes so opencode-go/<model> IDs match
@@ -192,7 +194,7 @@ export function loadPresetDefs(routingPath) {
192
194
  }
193
195
  let routing
194
196
  try {
195
- routing = yaml.load(raw)
197
+ routing = load(raw)
196
198
  } catch (err) {
197
199
  throw new Error(`presets: failed to load routing.yml: ${err.message}`)
198
200
  }
@@ -209,39 +211,10 @@ export function loadPresetDefs(routingPath) {
209
211
  function loadRoutingYaml(routingPath) {
210
212
  const path = routingPath ?? fileURLToPath(new URL('../routing.yml', import.meta.url))
211
213
  const raw = readFileSync(path, 'utf8')
212
- const routing = yaml.load(raw)
214
+ const routing = load(raw)
213
215
  return routing && typeof routing === 'object' ? routing : null
214
216
  }
215
217
 
216
- /**
217
- * Load the R4 per-agent step caps from routing.yml
218
- * (`agents.<name>.max_steps`). Fail-open: missing config yields {}.
219
- *
220
- * @param {object} [opts]
221
- * @param {string} [opts.routingPath]
222
- * @param {{warn?: Function}} [opts.logger]
223
- * @returns {Record<string, number>} lowercase agent → max_steps
224
- */
225
- export function loadRoutingMaxSteps({ routingPath, logger = console } = {}) {
226
- try {
227
- const routing = loadRoutingYaml(routingPath)
228
- const agents = routing?.agents
229
- if (!agents || typeof agents !== 'object') return {}
230
- const out = {}
231
- for (const [agent, spec] of Object.entries(agents)) {
232
- const maxSteps = spec && typeof spec === 'object' ? spec.max_steps : undefined
233
- if (Number.isSafeInteger(maxSteps) && maxSteps > 0) {
234
- out[agent.toLowerCase()] = maxSteps
235
- }
236
- }
237
- return out
238
- } catch (err) {
239
- const reason = err instanceof Error ? err.message : String(err)
240
- logger.warn?.(`presets: routing.yml max_steps unavailable (${reason})`)
241
- return {}
242
- }
243
- }
244
-
245
218
  /**
246
219
  * Load the O5 permission.task glob rules from routing.yml
247
220
  * (`permission.task:`). Fail-open: missing config yields null (caller keeps