pantheon-opencode 1.6.0-beta.7 → 1.6.0-beta.8

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 (49) hide show
  1. package/AGENTS.md +58 -15
  2. package/CHANGELOG.md +37 -100
  3. package/README.md +78 -102
  4. package/README.pt-BR.md +19 -37
  5. package/package.json +1 -2
  6. package/plugin.json +2 -2
  7. package/pyproject.toml +1 -1
  8. package/scripts/build-agents-md.mjs +2 -3
  9. package/scripts/doctor.mjs +22 -104
  10. package/scripts/hooks/scan-secrets.sh +97 -0
  11. package/scripts/install/config-migration.mjs +9 -42
  12. package/scripts/install/interactive.mjs +1 -1
  13. package/scripts/install/opencode.mjs +114 -43
  14. package/scripts/install/strings.mjs +0 -8
  15. package/scripts/probe-pantheon-cost.mjs +3 -16
  16. package/scripts/test-opencode-v2-sandbox.sh +203 -786
  17. package/scripts/validate-sandbox-install.mjs +138 -0
  18. package/skills-lock.json +3 -4
  19. package/src/agents/aphrodite.md +2 -9
  20. package/src/agents/demeter.md +0 -9
  21. package/src/agents/gaia.md +1 -0
  22. package/src/agents/hephaestus.md +0 -9
  23. package/src/agents/hermes.md +2 -9
  24. package/src/agents/mnemosyne.md +14 -42
  25. package/src/agents/prometheus.md +0 -9
  26. package/src/agents/themis.md +0 -1
  27. package/src/agents/zeus.md +180 -19
  28. package/src/instructions/agent-return-format.instructions.md +48 -3
  29. package/src/instructions/memory-protocol.instructions.md +8 -9
  30. package/src/instructions/zeus-anti-stall.instructions.md +54 -6
  31. package/src/instructions/zeus-council-synthesis.instructions.md +71 -0
  32. package/src/instructions/zeus-memory-operations.instructions.md +4 -4
  33. package/src/instructions/zeus-timeout-retry.instructions.md +81 -7
  34. package/src/pantheon/v2-delegation-enforce.ts +2 -1
  35. package/src/plugin-v2.ts +251 -168
  36. package/src/plugins/pantheon-hooks.ts +35 -82
  37. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  38. package/src/plugins/tui/dist/tui.js +26 -50
  39. package/src/plugins/tui/package-lock.json +2 -2
  40. package/src/plugins/tui/package.json +1 -1
  41. package/src/plugins/tui/src/index.tsx +25 -60
  42. package/src/routing.yml +4 -2
  43. package/src/skills/README.md +7 -12
  44. package/src/skills/agent-coordination/SKILL.md +83 -13
  45. package/src/skills/auto-continue/SKILL.md +13 -14
  46. package/src/skills/context-compression/SKILL.md +26 -879
  47. package/src/pantheon/secret-scanner.ts +0 -489
  48. package/src/plugins/tui/README.md +0 -14
  49. package/src/skills/council-synthesis/SKILL.md +0 -29
@@ -135,6 +135,74 @@ export const MANAGED_FIELDS = Object.freeze([
135
135
  'disable_model_invocation',
136
136
  ])
137
137
 
138
+ const PANTHEON_MCP_SERVERS = Object.freeze([
139
+ 'pantheon-resources',
140
+ 'pantheon-memory',
141
+ 'pantheon-code-mode',
142
+ 'pantheon-persistence',
143
+ 'pantheon-vision',
144
+ ])
145
+
146
+ function normalizeMcpPermissionSegment(value) {
147
+ return value.replace(/[^a-zA-Z0-9_]/g, '_')
148
+ }
149
+
150
+ /**
151
+ * Translate an agent's `mcp_tools` frontmatter into per-tool native
152
+ * permissions. V1 matches the original server-tool names; V2 normalizes
153
+ * punctuation to underscores. Emitting both spellings keeps the same source
154
+ * metadata valid for either host. Unlisted bundled Pantheon servers default to
155
+ * deny; unrelated user MCP servers are not touched.
156
+ *
157
+ * @param {Record<string, 'all' | string[]>} mcpTools
158
+ * @returns {Record<string, 'allow' | 'deny'>}
159
+ */
160
+ export function deriveNativeMcpPermissions(mcpTools) {
161
+ if (mcpTools === null || typeof mcpTools !== 'object' || Array.isArray(mcpTools)) {
162
+ throw new TypeError('mcp_tools must be an object keyed by MCP server name')
163
+ }
164
+
165
+ const servers = new Set([...PANTHEON_MCP_SERVERS, ...Object.keys(mcpTools)])
166
+ const permissions = {}
167
+
168
+ for (const server of servers) {
169
+ if (!/^[a-zA-Z0-9._-]+$/.test(server)) {
170
+ throw new TypeError(`mcp_tools server name is invalid: ${server}`)
171
+ }
172
+ const tools = Object.hasOwn(mcpTools, server) ? mcpTools[server] : []
173
+ if (
174
+ tools !== 'all' &&
175
+ (!Array.isArray(tools) ||
176
+ tools.some((tool) => typeof tool !== 'string' || !/^[a-zA-Z0-9._-]+$/.test(tool)))
177
+ ) {
178
+ throw new TypeError(`mcp_tools.${server} must be "all" or an array of tool names`)
179
+ }
180
+
181
+ const serverAliases = new Set([server, normalizeMcpPermissionSegment(server)])
182
+ for (const alias of serverAliases) permissions[`${alias}_*`] = 'deny'
183
+ }
184
+
185
+ for (const server of servers) {
186
+ const tools = Object.hasOwn(mcpTools, server) ? mcpTools[server] : []
187
+ const serverAliases = new Set([server, normalizeMcpPermissionSegment(server)])
188
+ if (tools === 'all') {
189
+ for (const alias of serverAliases) permissions[`${alias}_*`] = 'allow'
190
+ continue
191
+ }
192
+
193
+ for (const tool of tools) {
194
+ const toolAliases = new Set([tool, normalizeMcpPermissionSegment(tool)])
195
+ for (const serverAlias of serverAliases) {
196
+ for (const toolAlias of toolAliases) {
197
+ permissions[`${serverAlias}_${toolAlias}`] = 'allow'
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ return permissions
204
+ }
205
+
138
206
  function deepClone(value) {
139
207
  return JSON.parse(JSON.stringify(value))
140
208
  }
@@ -226,26 +294,23 @@ function mergeMissing(target, source) {
226
294
  }
227
295
 
228
296
  /**
229
- * Resolve a plugin ref from the repository-root opencode.json to its stable
230
- * installed form.
297
+ * Resolve a plugin ref from the repository-root opencode.json to a path INSIDE
298
+ * the installed package.
231
299
  *
232
300
  * Only exact Pantheon plugin identities are resolved into the installed
233
301
  * package. In particular, a third-party absolute path ending in
234
302
  * `src/plugin.ts` or `src/plugins/pantheon-hooks.ts` is not evidence that the
235
- * plugin belongs to Pantheon and must be preserved verbatim. V1 remains a pair
236
- * of package-local paths; V2 uses an exact npm version so it is independent of
237
- * the installer's global prefix or transient npx cache.
303
+ * plugin belongs to Pantheon and must be preserved verbatim.
238
304
  */
239
305
  export function resolveInstalledPlugin(plugin) {
240
306
  if (typeof plugin !== 'string') return plugin
241
307
  const normalized = normalizePluginRef(plugin)
242
- if (normalized === PANTHEON_V2_PLUGIN || normalized === PANTHEON_V2_LEGACY_FILE) {
243
- return PANTHEON_V2_PACKAGE_SPEC
244
- }
245
308
  if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return join(ROOT, normalized)
309
+ // Legacy V2 file ref (pre-directory-contract): migrate to the installed
310
+ // plugin directory so the beta loader accepts the entry.
311
+ if (normalized === PANTHEON_V2_LEGACY_FILE) return join(ROOT, PANTHEON_V2_PLUGIN)
246
312
  const identity = managedPluginIdentity(plugin)
247
313
  if (identity !== null && isAbsolute(normalized)) {
248
- if (identity === PANTHEON_V2_PLUGIN) return PANTHEON_V2_PACKAGE_SPEC
249
314
  return INSTALLED_PANTHEON_PLUGIN_PATHS.get(identity) ?? plugin
250
315
  }
251
316
  return plugin
@@ -267,14 +332,7 @@ const PANTHEON_V2_PLUGIN = 'src/plugin-v2'
267
332
  const PANTHEON_V2_LEGACY_FILE = 'src/plugin-v2.ts'
268
333
  const PANTHEON_V1_EXPORT = 'pantheon-opencode/plugin'
269
334
  const PANTHEON_V2_EXPORT = 'pantheon-opencode/plugin-v2'
270
- const PACKAGE_MANIFEST = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
271
- const PANTHEON_V2_PACKAGE_SPEC = `${PACKAGE_MANIFEST.name}@${PACKAGE_MANIFEST.version}`
272
- // Installation origin probe: npm copies (global prefix, npx cache) always live
273
- // under a `node_modules` directory; a repo checkout does not. From a checkout —
274
- // or between a version bump and its publish — the V2 pin above names a version
275
- // that may not exist on the npm registry yet, and the host cannot resolve it
276
- // until publish. Advisory only: the install itself is structurally correct.
277
- const RUNNING_FROM_NPM_INSTALL = import.meta.url.includes('/node_modules/')
335
+ const PANTHEON_PACKAGE_REF = /^pantheon-opencode(?:@[^/]+)?(?:\/(plugin|plugin-v2))?$/
278
336
  const PANTHEON_PLUGIN_IDENTITIES = new Set([
279
337
  PANTHEON_V1_PLUGIN,
280
338
  PANTHEON_V1_HOOKS,
@@ -303,14 +361,22 @@ function managedPluginIdentity(ref) {
303
361
  const normalized = normalizePluginRef(ref)
304
362
  if (typeof normalized !== 'string') return null
305
363
 
364
+ // Older installs registered the package root itself (often pinned to a
365
+ // beta version) in OpenCode's plural `plugins` list. Treat all exact npm
366
+ // specs for this package as managed so an update removes the old cached
367
+ // copy before registering the current package path. Do not match similarly
368
+ // named third-party packages such as `pantheon-opencode-fork`.
369
+ const packageRef = normalized.match(PANTHEON_PACKAGE_REF)
370
+ if (packageRef !== null) {
371
+ if (packageRef[1] === 'plugin') return PANTHEON_V1_PLUGIN
372
+ return PANTHEON_V2_PLUGIN
373
+ }
374
+
306
375
  if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return normalized
307
376
  if (normalized === PANTHEON_V2_LEGACY_FILE) return PANTHEON_V2_PLUGIN
308
377
  if (normalized === PANTHEON_V1_EXPORT || normalized.startsWith(`${PANTHEON_V1_EXPORT}@`)) {
309
378
  return PANTHEON_V1_PLUGIN
310
379
  }
311
- if (normalized === PACKAGE_MANIFEST.name || normalized.startsWith(`${PACKAGE_MANIFEST.name}@`)) {
312
- return PANTHEON_V2_PLUGIN
313
- }
314
380
  if (normalized === PANTHEON_V2_EXPORT || normalized.startsWith(`${PANTHEON_V2_EXPORT}@`)) {
315
381
  return PANTHEON_V2_PLUGIN
316
382
  }
@@ -1154,6 +1220,12 @@ export async function installOpenCode(
1154
1220
  if (fm.permission) {
1155
1221
  agent.permission = JSON.parse(JSON.stringify(fm.permission))
1156
1222
  }
1223
+ if (fm.mcp_tools !== undefined) {
1224
+ agent.permission = {
1225
+ ...(agent.permission ?? {}),
1226
+ ...deriveNativeMcpPermissions(fm.mcp_tools),
1227
+ }
1228
+ }
1157
1229
 
1158
1230
  config[name] = agent
1159
1231
  }
@@ -1266,12 +1338,16 @@ export async function installOpenCode(
1266
1338
  // --------------------------------------------------------------------
1267
1339
  // B.5.1 Hermetic plugin resolution (packaging fix)
1268
1340
  // --------------------------------------------------------------------
1269
- // A developer running from a checkout may have a repository-root
1270
- // opencode.json with machine-specific plugin paths. V1 rewrites its two
1271
- // managed entries to paths under the installed package. V2 instead writes
1272
- // the exact package-root npm spec so it survives global-prefix and npx-cache
1273
- // changes; a source-checkout V2 install warns that the version may not yet
1274
- // exist on npm. Third-party paths remain untouched in either generation.
1341
+ // A developer running from a checkout has a repository-root opencode.json
1342
+ // that may reference plugins via developer-machine absolute paths (e.g.
1343
+ // <dev>/pantheon/.../src/plugin.ts or
1344
+ // <dev>/pantheon/.../src/plugins/pantheon-hooks.ts). Copying those
1345
+ // verbatim into the user's config would break every global install on any
1346
+ // other machine. resolveInstalledPlugin() rewrites each entry to the plugin
1347
+ // file inside the INSTALLED package (derived from ROOT — never hardcoded),
1348
+ // so both dev and global installs resolve to a path that actually exists.
1349
+ // The source opencode.json keeps its dev path for local development; the
1350
+ // transform happens at install/sync time only.
1275
1351
 
1276
1352
  if (version === 'v1') {
1277
1353
  // V1 loads the singular `plugin` list. Remove every Pantheon V2 entry from
@@ -1332,13 +1408,18 @@ export async function installOpenCode(
1332
1408
  }
1333
1409
  }
1334
1410
  }
1335
- // V2 plugin entries are resolved by OpenCode as npm packages. Pin the
1336
- // exact current version and use the package-root export, which is the V2
1337
- // default plugin. Unlike an absolute path into ROOT, this survives moving
1338
- // between a global install and an npx cache; unlike an unpinned package,
1339
- // it cannot silently change underneath an existing config. Legacy local
1340
- // directory/file refs are recognized via managedPluginIdentity and are
1341
- // replaced here.
1411
+ // The beta resolves every `plugins` entry through npm install, so an
1412
+ // `a/b` shorthand (e.g. the published `pantheon-opencode/plugin-v2`
1413
+ // export) is treated as a GitHub repo and dies with NpmInstallFailedError
1414
+ // (beta log evidence). The beta loader additionally requires each entry
1415
+ // to be a DIRECTORY with a real index.js/index.ts — a file path warns
1416
+ // "must be a directory" and a symlink is ignored (sandbox probe
1417
+ // evidence). Register the V2 plugin DIRECTORY inside the INSTALLED
1418
+ // package — exactly like V1's ensurePantheonPlugin, but pointing at
1419
+ // src/plugin-v2 whose index.ts shim re-exports the real entry.
1420
+ // Legacy refs (npm shorthand, the old src/plugin-v2.ts file path) are
1421
+ // recognized via managedPluginIdentity and already stripped above, so
1422
+ // they migrate here.
1342
1423
  const resolvedV2 = resolveInstalledPlugin(PANTHEON_V2_PLUGIN)
1343
1424
  config.plugins = config.plugins.filter(
1344
1425
  (plugin) => pluginReferenceIdentity(plugin) !== pluginReferenceIdentity(resolvedV2),
@@ -1522,16 +1603,6 @@ export async function installOpenCode(
1522
1603
  configToWrite.plugins,
1523
1604
  config.context_sandbox,
1524
1605
  )
1525
-
1526
- // Bootstrap chicken-and-egg: the pin names the CURRENT package version,
1527
- // which exists on the registry only after publish. From an npm install
1528
- // (global prefix / npx cache) that is the published version by
1529
- // construction; from a repo checkout the pin may point at an unpublished
1530
- // version the host cannot resolve. Warn instead of failing — the config is
1531
- // structurally correct and the pin realigns once the version is out.
1532
- if (!RUNNING_FROM_NPM_INSTALL) {
1533
- warning(S.v2UnpublishedPin(PANTHEON_V2_PACKAGE_SPEC))
1534
- }
1535
1606
  }
1536
1607
 
1537
1608
  const configContent = `${JSON.stringify(configToWrite, null, 2)}\n`
@@ -47,10 +47,6 @@ const TABLE = {
47
47
  prereqMissing:
48
48
  'runtime prerequisites missing — install the tools above, or re-run with --no-mcp to install without Python MCP servers',
49
49
  v2Migrating: 'Migrating config to V2 native format...',
50
- v2UnpublishedPin: (spec) =>
51
- `V2 plugin is pinned to ${spec}, but this installer is running from a source checkout — ` +
52
- 'that version may not be published to npm yet, so OpenCode cannot resolve the pin until it is. ' +
53
- 'Run the installer from the published package (npx pantheon-opencode init) to avoid this.',
54
50
  // summary (printSummary)
55
51
  summaryInstalled: (target) => `OpenCode installed in ${target}`,
56
52
  summaryWithErrors: (target) => `Installation finished WITH ERRORS in ${target}`,
@@ -99,10 +95,6 @@ const TABLE = {
99
95
  prereqMissing:
100
96
  'pré-requisitos do runtime ausentes — instale as ferramentas acima, ou rode de novo com --no-mcp para instalar sem os MCPs Python',
101
97
  v2Migrating: 'Migrando config para o formato nativo V2...',
102
- v2UnpublishedPin: (spec) =>
103
- `Plugin V2 fixado em ${spec}, mas este installer está rodando de um checkout do código-fonte — ` +
104
- 'essa versão pode ainda não estar publicada no npm, então o OpenCode não resolve o pin até a publicação. ' +
105
- 'Rode o installer pelo pacote publicado (npx pantheon-opencode init) para evitar isso.',
106
98
  // summary (printSummary)
107
99
  summaryInstalled: (target) => `OpenCode instalado em ${target}`,
108
100
  summaryWithErrors: (target) => `Instalação concluída COM ERROS em ${target}`,
@@ -8,10 +8,9 @@
8
8
  * temporary synthetic opencode database. A missing fixture runtime is also
9
9
  * AMBIENTAL rather than a false PASS.
10
10
  */
11
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs'
11
+ import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
12
12
  import { tmpdir } from 'node:os'
13
13
  import { join } from 'node:path'
14
- import { fileURLToPath, pathToFileURL } from 'node:url'
15
14
 
16
15
  const requestedVersion = valueFor('--version')
17
16
  const jsonOutput = process.argv.includes('--json')
@@ -152,27 +151,19 @@ async function main() {
152
151
  return
153
152
  }
154
153
 
155
- const root = mkdtempSync(join(tmpdir(), 'pantheon-cost-probe-'))
156
154
  let DatabaseSync
157
155
  let createCostCommand
158
156
  try {
159
157
  ;({ DatabaseSync } = await import('node:sqlite'))
160
- // Node's built-in TS stripping deliberately refuses files under
161
- // node_modules. Exercise the shipped command from an isolated temp copy
162
- // while resolving its runtime dependencies through the installed package.
163
- const runtimeSource = join(root, 'cost-command.ts')
164
- const packageNodeModules = fileURLToPath(new URL('../node_modules/', import.meta.url))
165
- copyFileSync(fileURLToPath(source), runtimeSource)
166
- symlinkSync(packageNodeModules, join(root, 'node_modules'), 'dir')
167
- ;({ createCostCommand } = await import(pathToFileURL(runtimeSource).href))
158
+ ;({ createCostCommand } = await import(source.href))
168
159
  } catch (error) {
169
- rmSync(root, { recursive: true, force: true })
170
160
  ambiental(
171
161
  `pantheon_cost fixture runtime unavailable: ${error instanceof Error ? error.message : String(error)}`,
172
162
  )
173
163
  return
174
164
  }
175
165
 
166
+ const root = mkdtempSync(join(tmpdir(), 'pantheon-cost-probe-'))
176
167
  const dataHome = join(root, 'data')
177
168
  const opencodeDir = join(dataHome, 'opencode')
178
169
  // Every real installation writes `opencode.db`; the v1/v2 distinction is the
@@ -263,7 +254,6 @@ async function main() {
263
254
 
264
255
  const restore = setEnvironment({
265
256
  XDG_DATA_HOME: dataHome,
266
- OPENCODE_DB: undefined,
267
257
  PANTHEON_OPENCODE_VERSION: requestedVersion,
268
258
  PANTHEON_COST_DB: undefined,
269
259
  })
@@ -294,7 +284,6 @@ async function main() {
294
284
 
295
285
  const envRestore = setEnvironment({
296
286
  XDG_DATA_HOME: dataHome,
297
- OPENCODE_DB: undefined,
298
287
  PANTHEON_OPENCODE_VERSION: requestedVersion,
299
288
  PANTHEON_COST_DB: envDb,
300
289
  })
@@ -319,7 +308,6 @@ async function main() {
319
308
 
320
309
  const explicitRestore = setEnvironment({
321
310
  XDG_DATA_HOME: dataHome,
322
- OPENCODE_DB: undefined,
323
311
  PANTHEON_OPENCODE_VERSION: requestedVersion,
324
312
  PANTHEON_COST_DB: envDb,
325
313
  })
@@ -341,7 +329,6 @@ async function main() {
341
329
 
342
330
  const missingRestore = setEnvironment({
343
331
  XDG_DATA_HOME: join(root, 'missing-data'),
344
- OPENCODE_DB: undefined,
345
332
  PANTHEON_OPENCODE_VERSION: requestedVersion,
346
333
  PANTHEON_COST_DB: undefined,
347
334
  })