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.
- package/.pantheon/code-mode/README.md +4 -2
- package/.pantheon/code-mode/checkpoint_session.py +138 -102
- package/.pantheon/code-mode/compress-inline.py +102 -57
- package/.pantheon/code-mode/manifest.json +2 -8
- package/CHANGELOG.md +59 -0
- package/README.md +115 -55
- package/README.pt-BR.md +86 -43
- package/commands/pantheon-optimize.md +2 -2
- package/package.json +13 -1
- package/plugin.json +1 -1
- package/pyproject.toml +8 -15
- package/scripts/build-agents-md.mjs +2 -2
- package/scripts/check-plugin-v2-coverage.mjs +213 -0
- package/scripts/ci-validate-yaml.py +7 -5
- package/scripts/code_mode_server.py +116 -24
- package/scripts/doctor.mjs +85 -28
- package/scripts/install/cli-ui.mjs +1 -1
- package/scripts/install/config-migration.mjs +124 -3
- package/scripts/install/health-check.mjs +6 -19
- package/scripts/install/interactive.mjs +6 -6
- package/scripts/install/migrate.mjs +1 -1
- package/scripts/install/opencode.mjs +43 -10
- package/scripts/mcp_persistence_server.py +390 -76
- package/scripts/memory_mcp_server.py +370 -196
- package/scripts/postinstall.mjs +0 -7
- package/scripts/probe-context-rehydrate.mjs +28 -11
- package/scripts/prune.mjs +2 -2
- package/scripts/test-opencode-v2-sandbox.sh +1759 -0
- package/scripts/validate-routing.mjs +40 -0
- package/skills-lock.json +9 -10
- package/src/agents/aphrodite.md +0 -1
- package/src/agents/apollo.md +0 -1
- package/src/agents/athena.md +0 -1
- package/src/agents/demeter.md +0 -1
- package/src/agents/gaia.md +0 -1
- package/src/agents/hephaestus.md +0 -1
- package/src/agents/hermes.md +0 -1
- package/src/agents/iris.md +0 -1
- package/src/agents/mnemosyne.md +15 -17
- package/src/agents/nyx.md +0 -1
- package/src/agents/prometheus.md +0 -1
- package/src/agents/talos.md +0 -1
- package/src/agents/themis.md +0 -1
- package/src/agents/zeus.md +1 -3
- package/src/mcp/code_mode_server.py +116 -24
- package/src/mcp/mcp_codemap_module.py +559 -129
- package/src/mcp/mcp_persistence_server.py +390 -76
- package/src/mcp/memory_mcp_server.py +371 -248
- package/src/mcp/pantheon_vision_server.py +136 -24
- package/src/mcp/requirements-mcp.txt +5 -3
- package/src/mcp/requirements-vision.txt +2 -0
- package/src/pantheon/model-command.ts +18 -7
- package/src/pantheon/presets.d.mts +0 -8
- package/src/pantheon/presets.mjs +0 -29
- package/src/pantheon/todo-enforcer.ts +1 -1
- package/src/pantheon/vision.ts +1 -1
- package/src/plugin-v2.ts +134 -104
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +23 -23
- package/src/plugins/tui/package.json +12 -12
- package/src/routing.yml +0 -16
- package/src/skills/README.md +31 -36
- package/src/skills/auto-continue/SKILL.md +27 -77
- package/.pantheon/code-mode/eval-llm-judge.py +0 -255
- package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
- package/.pantheon/code-mode/eval-run.py +0 -197
- package/.pantheon/code-mode/eval-static.py +0 -321
- package/.pantheon/code-mode/session-end-save.py +0 -186
- package/.pantheon/code-mode/session-end-save.sh +0 -18
- package/scripts/recover-zenodo-deposition.mjs +0 -146
- package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
- package/scripts/validate-zenodo-release.mjs +0 -154
- package/scripts/zenodo-artifact.mjs +0 -94
- package/scripts/zenodo-release-state.mjs +0 -53
- 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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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')
|
|
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(
|
|
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(
|
|
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')
|
|
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(
|
|
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(
|
|
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`
|