pantheon-opencode 1.5.2 → 1.6.0-beta.2

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 (75) hide show
  1. package/.pantheon/code-mode/README.md +4 -2
  2. package/.pantheon/code-mode/checkpoint_session.py +138 -102
  3. package/.pantheon/code-mode/compress-inline.py +102 -57
  4. package/.pantheon/code-mode/manifest.json +2 -8
  5. package/CHANGELOG.md +59 -0
  6. package/README.md +115 -55
  7. package/README.pt-BR.md +86 -43
  8. package/commands/pantheon-optimize.md +2 -2
  9. package/package.json +13 -1
  10. package/plugin.json +1 -1
  11. package/pyproject.toml +8 -15
  12. package/scripts/build-agents-md.mjs +2 -2
  13. package/scripts/check-plugin-v2-coverage.mjs +213 -0
  14. package/scripts/ci-validate-yaml.py +7 -5
  15. package/scripts/code_mode_server.py +116 -24
  16. package/scripts/doctor.mjs +85 -28
  17. package/scripts/install/cli-ui.mjs +1 -1
  18. package/scripts/install/config-migration.mjs +124 -3
  19. package/scripts/install/health-check.mjs +6 -19
  20. package/scripts/install/interactive.mjs +6 -6
  21. package/scripts/install/migrate.mjs +1 -1
  22. package/scripts/install/opencode.mjs +43 -10
  23. package/scripts/mcp_persistence_server.py +390 -76
  24. package/scripts/memory_mcp_server.py +370 -196
  25. package/scripts/postinstall.mjs +0 -7
  26. package/scripts/probe-context-rehydrate.mjs +28 -11
  27. package/scripts/prune.mjs +2 -2
  28. package/scripts/test-opencode-v2-sandbox.sh +1759 -0
  29. package/scripts/validate-routing.mjs +40 -0
  30. package/skills-lock.json +9 -10
  31. package/src/agents/aphrodite.md +0 -1
  32. package/src/agents/apollo.md +0 -1
  33. package/src/agents/athena.md +0 -1
  34. package/src/agents/demeter.md +0 -1
  35. package/src/agents/gaia.md +0 -1
  36. package/src/agents/hephaestus.md +0 -1
  37. package/src/agents/hermes.md +0 -1
  38. package/src/agents/iris.md +0 -1
  39. package/src/agents/mnemosyne.md +15 -17
  40. package/src/agents/nyx.md +0 -1
  41. package/src/agents/prometheus.md +0 -1
  42. package/src/agents/talos.md +0 -1
  43. package/src/agents/themis.md +0 -1
  44. package/src/agents/zeus.md +1 -3
  45. package/src/mcp/code_mode_server.py +116 -24
  46. package/src/mcp/mcp_codemap_module.py +559 -129
  47. package/src/mcp/mcp_persistence_server.py +390 -76
  48. package/src/mcp/memory_mcp_server.py +371 -248
  49. package/src/mcp/pantheon_vision_server.py +136 -24
  50. package/src/mcp/requirements-mcp.txt +5 -3
  51. package/src/mcp/requirements-vision.txt +2 -0
  52. package/src/pantheon/model-command.ts +18 -7
  53. package/src/pantheon/presets.d.mts +0 -8
  54. package/src/pantheon/presets.mjs +0 -29
  55. package/src/pantheon/todo-enforcer.ts +1 -1
  56. package/src/pantheon/vision.ts +1 -1
  57. package/src/plugin-v2.ts +134 -104
  58. package/src/plugins/tui/dist/tui.js +1 -1
  59. package/src/plugins/tui/package-lock.json +23 -23
  60. package/src/plugins/tui/package.json +12 -12
  61. package/src/routing.yml +0 -16
  62. package/src/skills/README.md +31 -36
  63. package/src/skills/auto-continue/SKILL.md +27 -77
  64. package/.pantheon/code-mode/eval-llm-judge.py +0 -255
  65. package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
  66. package/.pantheon/code-mode/eval-run.py +0 -197
  67. package/.pantheon/code-mode/eval-static.py +0 -321
  68. package/.pantheon/code-mode/session-end-save.py +0 -186
  69. package/.pantheon/code-mode/session-end-save.sh +0 -18
  70. package/scripts/recover-zenodo-deposition.mjs +0 -146
  71. package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
  72. package/scripts/validate-zenodo-release.mjs +0 -154
  73. package/scripts/zenodo-artifact.mjs +0 -94
  74. package/scripts/zenodo-release-state.mjs +0 -53
  75. package/src/skills/orchestration-workflow/SKILL.md +0 -311
@@ -116,7 +116,7 @@ const PROVIDER_RENAME_REVERSE = Object.fromEntries(
116
116
  * V2 shape:
117
117
  * { package: "aisdk:ai-sdk-openai", settings: { baseURL: "...", apiKey: "..." }, models: { ... } }
118
118
  */
119
- function convertProviderV1toV2(name, config) {
119
+ function convertProviderV1toV2(_name, config) {
120
120
  const result = { ...config }
121
121
 
122
122
  // npm → package (with aisdk: prefix)
@@ -171,7 +171,7 @@ function convertProviderV1toV2(name, config) {
171
171
  /**
172
172
  * Convert a single V2 provider config back to V1.
173
173
  */
174
- function convertProviderV2toV1(name, config) {
174
+ function convertProviderV2toV1(_name, config) {
175
175
  const result = { ...config }
176
176
 
177
177
  // package → npm (strip aisdk: prefix)
@@ -304,6 +304,114 @@ const V2_TO_V1_RENAMES = Object.fromEntries(
304
304
  Object.entries(V1_TO_V2_RENAMES).map(([k, v]) => [v, k]),
305
305
  )
306
306
 
307
+ /**
308
+ * Agent fields owned by the framework. Mirrors the exported MANAGED_FIELDS in
309
+ * install/opencode.mjs plus `source`, which the installer writes next to them.
310
+ * The migration runs before that merge, so it has to apply the same precedence
311
+ * itself when both `agent` and `agents` coexist: these come from the V1 block,
312
+ * every other field stays the user's.
313
+ *
314
+ * `permissions` is the V2 name — the converter renames the V1 `permission`
315
+ * block before the merge runs.
316
+ *
317
+ * `description` is deliberately absent, and this list is deliberately narrower
318
+ * than what the installer writes. The installer writes a description only when
319
+ * it creates an agent (install/opencode.mjs, the new-agent branch); on an agent
320
+ * that already exists it leaves the value alone, under its own comment "Existing
321
+ * agent fields belong to the user". So a description is framework-owned for a
322
+ * NEW agent and user-owned for an EXISTING one. Listing it here would make the
323
+ * migration stricter than the installer and would overwrite a description the
324
+ * installer deliberately preserves. mergeManagedAgents carries the value across
325
+ * instead, so it survives the merge without being treated as framework-owned.
326
+ *
327
+ * KNOWN DEBT: the installer still writes the V1 spelling `permission`, while V2
328
+ * reads `permissions`. An agent that goes through this merge can therefore end
329
+ * up carrying both keys, with `permission` left as dead weight that nothing
330
+ * reads. Tracked for the next slice; not fixed here, because picking a winner
331
+ * changes what an install writes.
332
+ */
333
+ export const MANAGED_AGENT_FIELDS = Object.freeze([
334
+ 'source',
335
+ 'temperature',
336
+ 'color',
337
+ 'permissions',
338
+ 'mode',
339
+ 'hidden',
340
+ 'disable_model_invocation',
341
+ ])
342
+
343
+ /**
344
+ * Compare this module's managed-agent list against the installer's.
345
+ *
346
+ * The two merge paths have to agree: the migration reimplements the installer's
347
+ * merge because it runs before it. A field present on only one side is silent
348
+ * divergence — either the migration overwrites a field the installer leaves
349
+ * alone, or it stops mirroring one the installer now writes. Both directions
350
+ * matter, so the result is reported as a pair of differences rather than a
351
+ * boolean.
352
+ *
353
+ * Names are compared after the converter's renames, because that is the
354
+ * spelling the merge actually sees.
355
+ *
356
+ * @param {string[]} installerFields - MANAGED_FIELDS as exported by install/opencode.mjs
357
+ * @returns {{ onlyInMigration: string[], onlyInInstaller: string[] }}
358
+ */
359
+ export function managedAgentFieldDrift(installerFields) {
360
+ const toV2Name = (field) => V1_TO_V2_RENAMES[field] ?? field
361
+ const migration = new Set(MANAGED_AGENT_FIELDS.map(toV2Name))
362
+ const installer = new Set(installerFields.map(toV2Name))
363
+ return {
364
+ onlyInMigration: [...migration].filter((f) => !installer.has(f)).sort(),
365
+ onlyInInstaller: [...installer].filter((f) => !migration.has(f)).sort(),
366
+ }
367
+ }
368
+
369
+ /** @returns {boolean} true for a plain name→config map (not null/array) */
370
+ function isAgentMap(value) {
371
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
372
+ }
373
+
374
+ /**
375
+ * Merge the converted V1 `agent` block into an existing V2 `agents` base.
376
+ *
377
+ * Agents present in both are merged field by field: the base is kept, keys the
378
+ * base does not have are filled in from the V1 block, and MANAGED_AGENT_FIELDS
379
+ * are overwritten by the V1 values. Agents present in only one block are taken
380
+ * as they are — a union, not a replacement.
381
+ *
382
+ * Filling the gaps matters, and it is not description-specific: any key present
383
+ * only in the V1 block used to vanish here — `model`, `provider` and `mcp` too.
384
+ * `description` is the visible case, because the installer never writes one for
385
+ * an existing agent, so the V1 block is the only place that value can come from
386
+ * and the agent lost it on every install, permanently. Filling gaps can never
387
+ * destroy a base value; on conflict the base still wins, as before.
388
+ *
389
+ * @param {object} base - existing V2 `agents` map
390
+ * @param {object} managed - converted V1 `agent` map
391
+ * @returns {object} merged map
392
+ */
393
+ function mergeManagedAgents(base, managed) {
394
+ const merged = { ...base }
395
+
396
+ for (const [name, agentConfig] of Object.entries(managed)) {
397
+ const existing = merged[name]
398
+ if (!isAgentMap(existing) || !isAgentMap(agentConfig)) {
399
+ merged[name] = agentConfig
400
+ continue
401
+ }
402
+ const entry = { ...existing }
403
+ for (const [field, value] of Object.entries(agentConfig)) {
404
+ if (!(field in entry)) entry[field] = value
405
+ }
406
+ for (const field of MANAGED_AGENT_FIELDS) {
407
+ if (field in agentConfig) entry[field] = agentConfig[field]
408
+ }
409
+ merged[name] = entry
410
+ }
411
+
412
+ return merged
413
+ }
414
+
307
415
  // ---------------------------------------------------------------------------
308
416
  // Public API
309
417
  // ---------------------------------------------------------------------------
@@ -333,7 +441,20 @@ export function migrateV1toV2(config) {
333
441
  }
334
442
  agents[agentName] = ac
335
443
  }
336
- result.agents = agents
444
+ // An `agents` (V2) block may already exist — the installed opencode.json
445
+ // carries both, in either key order. Renaming onto it with a plain
446
+ // replace dropped the whole managed merge, so merge instead.
447
+ const base = result.agents
448
+ result.agents = isAgentMap(base) ? mergeManagedAgents(base, agents) : agents
449
+ } else if (key === 'agents') {
450
+ // V2 block: it is the base. The singular `agent` key may already have
451
+ // been converted above, in which case merge the two.
452
+ const managed = result.agents
453
+ if (isAgentMap(value)) {
454
+ result.agents = isAgentMap(managed) ? mergeManagedAgents(value, managed) : value
455
+ } else if (!isAgentMap(managed)) {
456
+ result.agents = value
457
+ }
337
458
  } else if (key === 'provider' && typeof value === 'object' && value !== null) {
338
459
  const providers = {}
339
460
  for (const [provName, provConfig] of Object.entries(value)) {
@@ -182,25 +182,12 @@ export function healthCheck(target, { dryRun = false, pythonTarget = target } =
182
182
  results.failed.push({ check: 'mcp-sdk', detail: 'NOT INSTALLED — all MCP servers will fail' })
183
183
  }
184
184
  }
185
- // Check 4: chromadb importable
186
- if (python) {
187
- const result = spawnSync(
188
- python,
189
- ['-c', 'from fastembed import TextEmbedding; print(TextEmbedding.__module__)'],
190
- { stdio: 'pipe' },
191
- )
192
- if (result.status === 0) {
193
- results.passed.push({
194
- check: 'chromadb',
195
- detail: result.stdout.toString().trim(),
196
- })
197
- } else {
198
- results.warnings.push({
199
- check: 'chromadb',
200
- detail: 'NOT INSTALLED — semantic search will fail',
201
- })
202
- }
203
- }
185
+ // Check 4: the memory server's only search backend is the stdlib's own
186
+ // SQLite FTS5 (check 5 below). The former "chromadb importable" probe
187
+ // imported fastembed, which the vector pipeline removal deleted, so it
188
+ // warned "semantic search will fail" on every install. Removed rather than
189
+ // repointed: check 5 already asserts the capability that matters, and a
190
+ // second sqlite3 import probe would tell us nothing new.
204
191
 
205
192
  // Check 5: SQLite FTS5 available
206
193
  if (python) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { emitKeypressEvents } from 'node:readline'
4
- import { colors, icons } from './cli-ui.mjs'
4
+ import { colors } from './cli-ui.mjs'
5
5
 
6
6
  const cursorHide = () => process.stdout.write('\x1b[?25l')
7
7
  const cursorShow = () => process.stdout.write('\x1b[?25h')
@@ -95,7 +95,7 @@ export async function promptMultiSelect(
95
95
  const lineCount = lines.length
96
96
  if (previousLineCount > 0) cursorUp(previousLineCount)
97
97
  clearDown()
98
- process.stdout.write(lines.join('\n') + '\n')
98
+ process.stdout.write(`${lines.join('\n')}\n`)
99
99
  previousLineCount = lineCount
100
100
  }
101
101
 
@@ -122,7 +122,7 @@ export async function promptMultiSelect(
122
122
  reject(new Error('Canceled'))
123
123
  }
124
124
 
125
- function handler(str, key) {
125
+ function handler(_str, key) {
126
126
  if (!key) return
127
127
  if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
128
128
  cancel()
@@ -184,7 +184,7 @@ export async function promptConfirm(message, { default: def = true } = {}) {
184
184
  reject(new Error('Canceled'))
185
185
  }
186
186
 
187
- function handler(str, key) {
187
+ function handler(_str, key) {
188
188
  if (!key) return
189
189
  if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
190
190
  cancel()
@@ -263,7 +263,7 @@ export async function promptList(options, { title = '' } = {}) {
263
263
  const lineCount = lines.length
264
264
  if (previousLineCount > 0) cursorUp(previousLineCount)
265
265
  clearDown()
266
- process.stdout.write(lines.join('\n') + '\n')
266
+ process.stdout.write(`${lines.join('\n')}\n`)
267
267
  previousLineCount = lineCount
268
268
  }
269
269
 
@@ -290,7 +290,7 @@ export async function promptList(options, { title = '' } = {}) {
290
290
  reject(new Error('Canceled'))
291
291
  }
292
292
 
293
- function handler(str, key) {
293
+ function handler(_str, key) {
294
294
  if (!key) return
295
295
  if ((key.ctrl && key.name === 'c') || key.name === 'escape') {
296
296
  cancel()
@@ -332,7 +332,7 @@ function main() {
332
332
  if (list) {
333
333
  const history = listAppliedMigrations(target)
334
334
  if (!history.currentVersion) {
335
- console.log(' \u2139\uFE0F No install state found in: ' + target)
335
+ console.log(` \u2139\uFE0F No install state found in: ${target}`)
336
336
  return
337
337
  }
338
338
  console.log(` Current version: ${history.currentVersion}`)
@@ -61,6 +61,33 @@ const COMPONENT_NAMES = [
61
61
  'runtime',
62
62
  ]
63
63
 
64
+ /**
65
+ * Per-agent fields owned by the framework: for an agent that already exists in
66
+ * the target config these are overwritten from the canonical source, and for a
67
+ * new agent they are the seed. Everything else on an existing entry is the
68
+ * user's and is left alone.
69
+ *
70
+ * `source` is written right next to these (for both new and existing agents) but
71
+ * is kept out of the loop so it can also be set independently — the migration in
72
+ * config-migration.mjs mirrors this list plus `source`, and tests assert the two
73
+ * stay equal as sets.
74
+ *
75
+ * KNOWN DEBT: `permission` is the V1 spelling. V2 reads `permissions`, so an
76
+ * agent can carry both keys after an install and the V1 one becomes dead weight.
77
+ * Tracked for the next slice; changing it here would change what an install
78
+ * writes.
79
+ *
80
+ * @type {readonly string[]}
81
+ */
82
+ export const MANAGED_FIELDS = Object.freeze([
83
+ 'temperature',
84
+ 'color',
85
+ 'permission',
86
+ 'mode',
87
+ 'hidden',
88
+ 'disable_model_invocation',
89
+ ])
90
+
64
91
  function deepClone(value) {
65
92
  return JSON.parse(JSON.stringify(value))
66
93
  }
@@ -836,7 +863,6 @@ export async function installOpenCode(
836
863
  if (fm.mode) agent.mode = fm.mode
837
864
  if (fm.hidden) agent.hidden = fm.hidden
838
865
  if (fm.temperature !== undefined) agent.temperature = fm.temperature
839
- if (fm.steps !== undefined) agent.steps = fm.steps
840
866
  // Support both hyphen (YAML) and underscore (JSON) key variants
841
867
  if (fm['disable-model-invocation'] !== undefined)
842
868
  agent.disable_model_invocation = fm['disable-model-invocation']
@@ -855,15 +881,6 @@ export async function installOpenCode(
855
881
 
856
882
  const canonicalAgentConfig = readAgentConfigFromCanonical()
857
883
  const agentSources = getAgentSources(agentPrefix)
858
- const MANAGED_FIELDS = [
859
- 'steps',
860
- 'temperature',
861
- 'color',
862
- 'permission',
863
- 'mode',
864
- 'hidden',
865
- 'disable_model_invocation',
866
- ]
867
884
 
868
885
  if (config.agent === undefined) config.agent = {}
869
886
 
@@ -1222,6 +1239,22 @@ export async function installOpenCode(
1222
1239
  if (version === 'v2') {
1223
1240
  info(S.v2Migrating)
1224
1241
  configToWrite = migrateV1toV2(config)
1242
+
1243
+ // Pantheon no longer sets a step ceiling. Any `steps` sitting on a managed
1244
+ // agent was written by an earlier install and never chosen by the user —
1245
+ // the merge above is additive by design (no delete path), so drop it here
1246
+ // and let the host default apply. This runs after the migration because
1247
+ // migrateV1toV2 copies the V1 `agent` block across verbatim (including any
1248
+ // stale `steps`), which is exactly how the value reappears in V2.
1249
+ // User-defined agents are out of scope and keep their value; the V1
1250
+ // singular `agent` block is deliberately untouched (V1 is retiring).
1251
+ const v2Agents = configToWrite.agents
1252
+ if (v2Agents && typeof v2Agents === 'object' && !Array.isArray(v2Agents)) {
1253
+ for (const name of Object.keys(canonicalAgentConfig)) {
1254
+ const entry = v2Agents[name]
1255
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) delete entry.steps
1256
+ }
1257
+ }
1225
1258
  }
1226
1259
 
1227
1260
  const configContent = `${JSON.stringify(configToWrite, null, 2)}\n`