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

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 (66) hide show
  1. package/AGENTS.md +58 -15
  2. package/CHANGELOG.md +61 -94
  3. package/README.md +81 -103
  4. package/README.pt-BR.md +19 -37
  5. package/commands/pantheon-audit.md +7 -1
  6. package/commands/pantheon-consolidate.md +1 -1
  7. package/commands/pantheon-focus.md +17 -0
  8. package/package.json +1 -2
  9. package/plugin.json +2 -2
  10. package/pyproject.toml +1 -1
  11. package/scripts/build-agents-md.mjs +2 -3
  12. package/scripts/doctor.mjs +22 -104
  13. package/scripts/hooks/scan-secrets.sh +97 -0
  14. package/scripts/install/config-migration.mjs +9 -42
  15. package/scripts/install/interactive.mjs +1 -1
  16. package/scripts/install/opencode.mjs +174 -77
  17. package/scripts/install/plugin.mjs +43 -9
  18. package/scripts/install/strings.mjs +0 -8
  19. package/scripts/probe-pantheon-cost.mjs +3 -16
  20. package/scripts/sync-artifacts.mjs +1 -0
  21. package/scripts/sync-tui.mjs +24 -3
  22. package/scripts/test-opencode-v2-sandbox.sh +203 -786
  23. package/scripts/validate-sandbox-install.mjs +138 -0
  24. package/skills-lock.json +4 -5
  25. package/src/agents/aphrodite.md +2 -9
  26. package/src/agents/demeter.md +0 -9
  27. package/src/agents/gaia.md +1 -0
  28. package/src/agents/hephaestus.md +0 -9
  29. package/src/agents/hermes.md +2 -9
  30. package/src/agents/mnemosyne.md +18 -53
  31. package/src/agents/prometheus.md +0 -9
  32. package/src/agents/themis.md +0 -1
  33. package/src/agents/zeus.md +180 -19
  34. package/src/instructions/agent-return-format.instructions.md +48 -3
  35. package/src/instructions/memory-protocol.instructions.md +8 -9
  36. package/src/instructions/zeus-anti-stall.instructions.md +54 -6
  37. package/src/instructions/zeus-council-synthesis.instructions.md +71 -0
  38. package/src/instructions/zeus-memory-operations.instructions.md +4 -4
  39. package/src/instructions/zeus-timeout-retry.instructions.md +81 -7
  40. package/src/pantheon/v2-delegation-enforce.ts +2 -1
  41. package/src/plugin-v2.ts +251 -168
  42. package/src/plugins/pantheon-hooks.ts +35 -82
  43. package/src/plugins/tui/dist/tui-v2.d.ts +9 -0
  44. package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
  45. package/src/plugins/tui/dist/tui-v2.js +151 -0
  46. package/src/plugins/tui/dist/tui-v2.js.map +1 -0
  47. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  48. package/src/plugins/tui/dist/tui.js +29 -53
  49. package/src/plugins/tui/index-v2.ts +6 -0
  50. package/src/plugins/tui/package-lock.json +5411 -1292
  51. package/src/plugins/tui/package.json +10 -7
  52. package/src/plugins/tui/src/index-v2.tsx +108 -0
  53. package/src/plugins/tui/src/index.tsx +28 -63
  54. package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
  55. package/src/plugins/tui/tsdown.config.ts +2 -2
  56. package/src/plugins/tui/tui-v2.ts +2 -0
  57. package/src/plugins/tui/tui.ts +3 -0
  58. package/src/routing.yml +4 -2
  59. package/src/skills/README.md +7 -12
  60. package/src/skills/agent-coordination/SKILL.md +83 -13
  61. package/src/skills/auto-continue/SKILL.md +13 -14
  62. package/src/skills/context-compression/SKILL.md +26 -879
  63. package/src/skills/session-goal/SKILL.md +14 -5
  64. package/src/pantheon/secret-scanner.ts +0 -489
  65. package/src/plugins/tui/README.md +0 -14
  66. package/src/skills/council-synthesis/SKILL.md +0 -29
package/README.pt-BR.md CHANGED
@@ -53,19 +53,6 @@ Com o OpenCode em execução, descreva o resultado que você quer:
53
53
  ```
54
54
 
55
55
  O Pantheon ajuda a transformar esse pedido em um plano e em etapas revisadas.
56
- Perguntas sem mudança no repositório são respondidas diretamente; correções
57
- pequenas usam uma única delegação. Discovery, planejamento, paralelismo e
58
- revisão em council ficam para quando o escopo ou o risco justificarem.
59
-
60
- Para uma correção pequena e bem delimitada, chame `@talos` diretamente (por
61
- exemplo, `@talos corrija este typo`). Assim você pula a chamada de orquestração
62
- do Zeus; use `/pantheon` para planejamento, delegação, mudanças em vários
63
- arquivos ou gates de qualidade.
64
-
65
- O procedimento completo de council só é carregado em `/pantheon` ou quando uma
66
- decisão material realmente exige perspectivas distintas. Falhas de dispatch
67
- podem ser tentadas novamente uma vez; testes reprovados e recusas não são
68
- repetidos automaticamente.
69
56
 
70
57
  ## Para quem é?
71
58
 
@@ -84,13 +71,11 @@ verificações repetíveis e passagens claras entre etapas do trabalho.
84
71
 
85
72
  ## Status
86
73
 
87
- Versão candidata neste checkout: **v1.6.0-beta.7** (manifestos preparados;
88
- publicação ainda não confirmada). Para verificar a beta publicada, consulte a
89
- dist-tag `beta` do npm. O histórico permanece no [changelog](CHANGELOG.md). O
90
- Pantheon foi feito para OpenCode e depende da disponibilidade e da configuração
91
- do OpenCode e dos serviços opcionais que você escolher. Veja as
92
- [releases](https://github.com/ils15/pantheon-opencode/releases) e o
93
- [changelog](CHANGELOG.md) para acompanhar as mudanças publicadas.
74
+ Versão operacional neste checkout: **v1.6.0-beta.1** (primeira beta compatível
75
+ com OpenCode 2; esta página não afirma publicação). O Pantheon foi feito para OpenCode e depende da
76
+ disponibilidade e da configuração do OpenCode e dos serviços opcionais que você
77
+ escolher. Veja as [releases](https://github.com/ils15/pantheon-opencode/releases)
78
+ e o [changelog](CHANGELOG.md) para acompanhar as mudanças publicadas.
94
79
 
95
80
 
96
81
  ## Execução code-mode (opt-in explícito)
@@ -136,13 +121,13 @@ registrados juntos.
136
121
  | | V1 | V2 |
137
122
  |---|---|---|
138
123
  | Chave de config do OpenCode | `plugin` singular | `plugins` plural |
139
- | Registro Pantheon | `src/plugin.ts` mais `src/plugins/pantheon-hooks.ts` | pacote npm `pantheon-opencode@<versão>` fixado à versão instalada (export raiz aponta para `src/plugin-v2.ts`) |
140
- | Contrato de runtime | Plugin Pantheon V1: 6 ferramentas (`hashline_edit`, as 3 ferramentas de goal, `pantheon_cost`, `pantheon_model`), hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2: 3 ferramentas (`hashline_edit`, `pantheon_cost`, `pantheon_model`), 5 assinaturas de eventos, session hooks (`prompt`, `context`), hooks de execução/permissão e transforms de configuração |
124
+ | Registro Pantheon | `src/plugin.ts` mais `src/plugins/pantheon-hooks.ts` | `pantheon-opencode/plugin-v2` (`src/plugin-v2.ts`) |
125
+ | Contrato de runtime | Plugin Pantheon V1: 6 ferramentas (`hashline_edit`, as 3 ferramentas de goal, `pantheon_cost`, `pantheon_model`), hooks de evento/ferramenta e tratamento de compactação V1 | Plugin V2 completo: 3 ferramentas (`hashline_edit`, `pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos, session hooks (`prompt`, `context`), um hook de ferramenta `execute.before` que impõe sessões somente-leitura, além de transforms de configuração |
141
126
  | APIs V1 | Registradas | Definições próprias de ferramentas via `ctx.tool.transform()` — não pelo caminho do plugin V1 |
142
127
 
143
128
  O plugin V2 fornece 3 ferramentas de orquestração (`hashline_edit`,
144
- `pantheon_cost`, `pantheon_model`), 5 assinaturas de eventos (`session.created`,
145
- `session.idle`, `session.deleted`, `session.error`, `session.compacted`), session hooks (`prompt`,
129
+ `pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos (`session.created`,
130
+ `session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
146
131
  `context`) e um hook de ferramenta `execute.before` que impõe sessões
147
132
  somente-leitura. As 3 ferramentas de goal **não** são
148
133
  registradas no V2: o goal loop precisa de um `GoalStore`, de um
@@ -174,18 +159,15 @@ toda chamada. Os recursos V2 sem suporte são `legacy-hooks` (a superfície de
174
159
  hooks específica do V1), `catalog-transform`, `integration-transform`,
175
160
  `skill-transform` e `goal-tools`.
176
161
 
177
- Ao migrar uma config V1 com `init --opencode-version v2`, o instalador traduz
178
- `task` para a ação V2 `subagent` e `write` para `edit` (que também cobre as
179
- tools host `write`/`patch`). Se regras legadas `edit` e `write` colidirem no
180
- mesmo recurso, a migração preserva o efeito mais restritivo. Configs V2
181
- editadas manualmente devem usar o array nativo `permissions` com
182
- `action`/`resource`/`effect`.
162
+ O pacote expõe os dois contratos como exports importáveis:
163
+ `pantheon-opencode/plugin` (V1), `pantheon-opencode/plugin-v2` (V2) e
164
+ `pantheon-opencode/v2-bridge` (interop opcional), para que o host carregue
165
+ explicitamente o contrato desejado.
183
166
 
184
- O pacote expõe os dois contratos: a raiz e `pantheon-opencode/plugin-v2`
185
- carregam V2; `pantheon-opencode/plugin` carrega V1. `pantheon-opencode/v2-bridge`
186
- é atualmente apenas uma utility importável: o setup de produção não a conecta
187
- ao plugin V2, então ela não fornece interoperabilidade em runtime nem restaura
188
- as ferramentas de goal/board do V1.
167
+ A ponte V1→V2 (`src/pantheon/v2-bridge.ts`) habilita interop opcional:
168
+ singletons de infraestrutura V1 (BackgroundJobBoard,
169
+ GoalStore, TodoEnforcer, VisionHandler) são repassados via `ctx.options` do V2.
170
+ A ponte é opcional — o V2 funciona standalone com degradação graciosa.
189
171
 
190
172
  Selecione o contrato explicitamente na instalação:
191
173
 
@@ -262,8 +244,8 @@ pontos. A ordem das chaves não muda o resultado.
262
244
  Um comando mantém uma instalação existente em dia:
263
245
 
264
246
  ```bash
265
- # Sem instalação global, fixe o dist-tag desejado. `latest` aponta para a
266
- # release estável publicada mais recente, não para um candidato não publicado:
247
+ # Sem instalação global, SEMPRE fixe o dist-tag — `npx pantheon-opencode`
248
+ # puro resolve a tag `latest`, que é o release estável (1.4.3):
267
249
  npx pantheon-opencode@beta update # canal beta do npm + refresh do config
268
250
  npx pantheon-opencode@beta update --stable # canal estável
269
251
 
@@ -15,7 +15,13 @@ agent: "zeus"
15
15
  ## Layers
16
16
 
17
17
  ### Layer 1 — Heuristic Scan
18
- `python3 scripts/themis_heuristic_scan.py [--path=<dir>]`
18
+ Resolve the installed helper first (do not assume the current project contains
19
+ Pantheon source). Check, in order: `$OPENCODE_CONFIG_DIR/.opencode/scripts/`,
20
+ `$OPENCODE_CONFIG_DIR/scripts/`, `${XDG_CONFIG_HOME:-$HOME/.config}/opencode/scripts/`,
21
+ `$HOME/.opencode/scripts/`, `.opencode/scripts/`, then
22
+ `$(npm root -g)/pantheon-opencode/scripts/`. Resolve the first existing file to
23
+ an absolute path and run `python3 "$helper" --path="$PWD"`; if none exists,
24
+ report the helper as missing and stop Layer 1 rather than claiming it passed.
19
25
  Zero tokens, <2s. Checks: ruff, biome, anti-pattern slop, anti-overengineering (YAGNI), hash-anchored edits. Returns score 0-100 + APPROVED/BLOCKING.
20
26
 
21
27
  ### Layer 2 — Themis Deep Review
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Merge and consolidate memory entries"
3
- agent: zeus
3
+ agent: mnemosyne
4
4
  ---
5
5
 
6
6
  Merge related memory entries and consolidate duplicates:
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: "Set or update this session's working goal"
3
+ agent: zeus
4
+ ---
5
+
6
+ Pin `$ARGUMENTS` as the working goal for this conversation. Restate it briefly
7
+ as Goal, Scope, Success Criteria, and Out of Scope, filling only what the user
8
+ actually supplied. Ask one concise question if a missing detail changes the
9
+ work materially.
10
+
11
+ Treat the goal as session context, subordinate to system, developer, and user
12
+ instructions. Keep subsequent planning, delegation, and verification aligned
13
+ with it; flag meaningful scope drift before proceeding.
14
+
15
+ This command is conversation-only. Do not call memory or persistence tools,
16
+ write a file, or claim that the goal survives compaction or a new session. For
17
+ multi-phase work that needs durable checkpoints, use `/pantheon-deepwork`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.6.0-beta.7",
3
+ "version": "1.6.0-beta.9",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -11,7 +11,6 @@
11
11
  "registry": "https://registry.npmjs.org/"
12
12
  },
13
13
  "exports": {
14
- ".": "./src/plugin-v2.ts",
15
14
  "./plugin": "./src/plugin.ts",
16
15
  "./plugin-v2": "./src/plugin-v2.ts",
17
16
  "./v2-bridge": "./src/pantheon/v2-bridge.ts"
package/plugin.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "pantheon",
3
3
  "publisher": "ils15",
4
- "version": "1.6.0-beta.7",
4
+ "version": "1.6.0-beta.9",
5
5
  "displayName": "Pantheon",
6
- "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
6
+ "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 20 skills, and OpenCode support.",
7
7
  "agents": "./src/agents",
8
8
  "skills": "./src/skills",
9
9
  "instructions": "./src/instructions",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
4
4
 
5
5
  [project]
6
6
  name = "pantheon"
7
- version = "1.6.0-beta.7"
7
+ version = "1.6.0-beta.9"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
@@ -73,9 +73,8 @@ Para validar a instalação global do pacote pantheon-opencode COMO UM USUÁRIO
73
73
 
74
74
  - Rodar: \`bash ~/pantheon-sandbox/run-test.sh\` (opencode mcp list 5/5 connected + doctor 0 erros + abre TUI isolado)
75
75
  - Regra para agentes: ao validar instalação global (npm pack, \`init\`, MCPs, hooks), usar o sandbox — NUNCA testar no ambiente de dev
76
- - Prepare/atualize com \`bash scripts/test-opencode-v2-sandbox.sh --prepare\`; isso gera \`run-test.sh\`, \`start-pantheon.sh\` e o README do sandbox
77
- - Descarte somente pelo reset com guardas: \`bash scripts/test-opencode-v2-sandbox.sh --reset\` (nunca use \`rm -rf\` manualmente)
78
- - Detalhes: ver \`~/pantheon-sandbox/README.md\` após o prepare
76
+ - Descarte: \`rm -rf ~/pantheon-sandbox\`
77
+ - Detalhes: ver \`~/pantheon-sandbox/README.md\`
79
78
 
80
79
  ## Conventions
81
80
 
@@ -1200,58 +1200,33 @@ export function resolveMcpCwd(entry, configPath = '') {
1200
1200
  // ---------------------------------------------------------------------------
1201
1201
 
1202
1202
  /**
1203
- * Prerelease-aware comparison for pantheon versions (X.Y.Z with an optional
1204
- * semver prerelease suffix: `-beta.9`, `-rc.1`, ...). A stable release
1205
- * outranks any prerelease of the same core version; two prereleases compare
1206
- * identifier by identifier, numeric identifiers numerically. Returns
1203
+ * Prerelease-aware comparison for pantheon versions (X.Y.Z or X.Y.Z-beta.N).
1204
+ * A stable release outranks any beta of the same core version. Returns
1207
1205
  * <0, 0, >0 (a - b). The shared compareVersions/parseVersion above ignores
1208
- * the prerelease suffix (it only reads 4 numeric groups), which would make
1209
- * every prerelease pair compare equal.
1206
+ * the `-beta.N` suffix (it only reads 4 numeric groups), which would make
1207
+ * every beta pair compare equal.
1210
1208
  */
1211
1209
  function comparePantheonVersions(a, b) {
1212
1210
  const parse = (v) => {
1213
1211
  const m = String(v)
1214
1212
  .trim()
1215
- .match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/)
1213
+ .match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/)
1216
1214
  if (!m) return null
1217
1215
  return {
1218
1216
  core: [+m[1], +m[2], +m[3]],
1219
- prerelease: m[4] === undefined ? null : m[4].split('.'),
1217
+ beta: m[4] === undefined ? Number.POSITIVE_INFINITY : +m[4],
1220
1218
  }
1221
1219
  }
1222
- // Per semver precedence: numeric identifiers compare numerically and rank
1223
- // below alphanumeric ones; a shorter identifier list ranks below a longer
1224
- // one sharing its prefix; a version WITHOUT a prerelease outranks any
1225
- // prerelease of the same core version.
1226
- const comparePrerelease = (pa, pb) => {
1227
- if (pa === null && pb === null) return 0
1228
- if (pa === null) return 1
1229
- if (pb === null) return -1
1230
- const length = Math.max(pa.length, pb.length)
1231
- for (let i = 0; i < length; i++) {
1232
- const x = pa[i]
1233
- const y = pb[i]
1234
- if (x === undefined) return -1
1235
- if (y === undefined) return 1
1236
- const xNumeric = /^\d+$/.test(x)
1237
- const yNumeric = /^\d+$/.test(y)
1238
- if (xNumeric && yNumeric) {
1239
- if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1
1240
- } else if (xNumeric !== yNumeric) {
1241
- return xNumeric ? -1 : 1
1242
- } else if (x !== y) {
1243
- return x < y ? -1 : 1
1244
- }
1245
- }
1246
- return 0
1247
- }
1248
1220
  const pa = parse(a)
1249
1221
  const pb = parse(b)
1250
1222
  if (!pa || !pb) return 0
1251
1223
  for (let i = 0; i < 3; i++) {
1252
1224
  if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i]
1253
1225
  }
1254
- return comparePrerelease(pa.prerelease, pb.prerelease)
1226
+ // Infinity - Infinity is NaN, which would make equal stable versions compare
1227
+ // as drift for any caller using a strict === 0 check.
1228
+ if (pa.beta === pb.beta) return 0
1229
+ return pa.beta < pb.beta ? -1 : 1
1255
1230
  }
1256
1231
 
1257
1232
  /**
@@ -1317,24 +1292,8 @@ export function checkInstallVersionDrift(args) {
1317
1292
  // registered plugin points into against this package's own version. Without
1318
1293
  // network access the "latest" version IS the running package's version.
1319
1294
 
1320
- // Single source of truth: the package name is read from package.json, the same
1321
- // way scripts/install/opencode.mjs derives the V2 pin spec. The fallback keeps
1322
- // the doctor usable on a broken manifest.
1323
- const PACKAGE_MANIFEST = readJson(join(ROOT, 'package.json'))
1324
- const PANTHEON_PACKAGE_NAME =
1325
- typeof PACKAGE_MANIFEST?.name === 'string' && PACKAGE_MANIFEST.name
1326
- ? PACKAGE_MANIFEST.name
1327
- : 'pantheon-opencode'
1328
- // Matches the exact version-pinned V2 spec the installer writes
1329
- // (`<name>@X.Y.Z` plus any semver prerelease suffix). The name is escaped
1330
- // because package names may contain regex-significant characters (`.` in
1331
- // unscoped names, `/` in scoped ones).
1332
- const PINNED_PLUGIN_SPEC_PATTERN = new RegExp(
1333
- `^${PANTHEON_PACKAGE_NAME.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')}@(\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?)$`,
1334
- )
1335
-
1336
1295
  /** Locates an installed copy of this package inside a dependency tree. */
1337
- const INSTALLED_PACKAGE_MARKER = `/node_modules/${PANTHEON_PACKAGE_NAME}/`
1296
+ const INSTALLED_PACKAGE_MARKER = '/node_modules/pantheon-opencode/'
1338
1297
 
1339
1298
  /**
1340
1299
  * Return the installed-package root containing a plugin path, or null when the
@@ -1403,10 +1362,8 @@ export function collectRegisteredPluginRefs(configs) {
1403
1362
  if (typeof ref === 'string') {
1404
1363
  refs.push({ path: ref, configPath: cfg.path, configLabel: cfg.label })
1405
1364
  } else if (ref && typeof ref === 'object') {
1406
- // npm plugin entries carry their identity under `package`; local
1407
- // entries use `path`/`source`. Preserve either so H3 can inspect the
1408
- // exact pinned npm version as well as installed filesystem copies.
1409
- const objectPath = ['package', 'path', 'source', 'id', 'name'].find(
1365
+ // Object-shaped entries carry their path under `path`/`source`.
1366
+ const objectPath = ['path', 'source', 'id', 'name'].find(
1410
1367
  (k) => typeof ref[k] === 'string',
1411
1368
  )
1412
1369
  if (objectPath) {
@@ -1419,20 +1376,6 @@ export function collectRegisteredPluginRefs(configs) {
1419
1376
  return refs
1420
1377
  }
1421
1378
 
1422
- /**
1423
- * Read an exact version from the package-root plugin syntax written by the
1424
- * V2 installer (`pantheon-opencode@1.6.0`), including ANY semver prerelease
1425
- * suffix (`-beta.11`, `-rc.1`, ...). Unpinned and unrelated packages
1426
- * deliberately return null: H3 cannot infer their resolved npm cache version.
1427
- * @param {string} pluginRef
1428
- * @returns {string|null}
1429
- */
1430
- function readPinnedPantheonPluginVersion(pluginRef) {
1431
- if (typeof pluginRef !== 'string') return null
1432
- const match = pluginRef.match(PINNED_PLUGIN_SPEC_PATTERN)
1433
- return match?.[1] ?? null
1434
- }
1435
-
1436
1379
  /**
1437
1380
  * Compare the version of the package each registered plugin points into (when
1438
1381
  * it is an installed copy under node_modules/pantheon-opencode) against this
@@ -1461,21 +1404,6 @@ export function checkPluginVersionDrift(args) {
1461
1404
  let compared = 0
1462
1405
  let driftReported = false
1463
1406
  for (const ref of refs) {
1464
- const pinnedNpmVersion = readPinnedPantheonPluginVersion(ref.path)
1465
- if (pinnedNpmVersion) {
1466
- const identity = `npm:${PANTHEON_PACKAGE_NAME}@${pinnedNpmVersion}`
1467
- if (seen.has(identity)) continue
1468
- seen.add(identity)
1469
- compared++
1470
-
1471
- if (classifyPluginVersionDrift(pinnedNpmVersion, packageVersion) !== 'drift') continue
1472
- driftReported = true
1473
- warn(
1474
- `${ref.configLabel}: V2 plugin is pinned to ${PANTHEON_PACKAGE_NAME}@${pinnedNpmVersion}, but this package is v${packageVersion} — the registered tool surface may be stale. Run \`npx pantheon-opencode init\` to realign the pinned plugin version`,
1475
- )
1476
- continue
1477
- }
1478
-
1479
1407
  const absolute = isAbsolute(ref.path) ? ref.path : resolve(dirname(ref.configPath), ref.path)
1480
1408
  const installed = readInstalledPluginVersion(absolute)
1481
1409
  if (!installed) continue
@@ -1514,29 +1442,21 @@ export function checkPluginVersionDrift(args) {
1514
1442
  * The V2 contract is the PLURAL `plugins` key; V1 is the singular `plugin` key.
1515
1443
  * So the key alone is not the test — a config can legitimately carry a
1516
1444
  * third-party entry in `plugins`. What identifies the generation is a Pantheon
1517
- * V2 entry inside it. Recognises the canonical directory, an absolute path,
1518
- * the package subpath export, and the version-pinned package-root entry emitted
1519
- * by the installer. The exact package name is checked to avoid matching
1520
- * similarly named third-party plugins.
1445
+ * V2 entry inside it. Matched on the `plugin-v2` marker so all three real
1446
+ * spellings are recognised: the canonical DIRECTORY (`src/plugin-v2`), an
1447
+ * absolute path to it, and the package export (`pantheon-opencode/plugin-v2`).
1521
1448
  *
1522
1449
  * @param {unknown} ref
1523
1450
  * @returns {boolean}
1524
1451
  */
1525
1452
  function isV2PluginRef(ref) {
1526
- const candidates =
1453
+ const candidate =
1527
1454
  typeof ref === 'string'
1528
- ? [ref]
1455
+ ? ref
1529
1456
  : ref && typeof ref === 'object'
1530
- ? ['package', 'path', 'source', 'id', 'name']
1531
- .map((key) => ref[key])
1532
- .filter((value) => typeof value === 'string')
1533
- : []
1534
- return candidates.some(
1535
- (candidate) =>
1536
- candidate.includes('plugin-v2') ||
1537
- candidate === PANTHEON_PACKAGE_NAME ||
1538
- candidate.startsWith(`${PANTHEON_PACKAGE_NAME}@`),
1539
- )
1457
+ ? (ref.path ?? ref.source ?? ref.id ?? ref.name)
1458
+ : undefined
1459
+ return typeof candidate === 'string' && candidate.includes('plugin-v2')
1540
1460
  }
1541
1461
 
1542
1462
  /**
@@ -1912,9 +1832,7 @@ export function classifyNodeSqliteProbe(probe) {
1912
1832
  if (probe.available) {
1913
1833
  return {
1914
1834
  status: 'ok',
1915
- message:
1916
- `node:sqlite available on ${probe.version} — read-only SQLite backend prerequisite satisfied; ` +
1917
- 'doctor does not verify pantheon_cost tool registration',
1835
+ message: `node:sqlite available on ${probe.version} — pantheon_cost read-only backend supported`,
1918
1836
  }
1919
1837
  }
1920
1838
  return {
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # scan-secrets.sh — Pantheon PreToolUse Secret Scanning Hook
3
+ # Detects hardcoded secrets in tool input. Logs ALWAYS (masked); the exit
4
+ # code drives the plugin's hybrid blocking (see pantheon-hooks.ts):
5
+ #
6
+ # exit 0 — no match.
7
+ # exit 1 — LOW_CONFIDENCE match only (header/KEY NAMES such as the Bifrost header
8
+ # alone, api_key=/password=/secret= without a recognizable token
9
+ # value). Advisory: the plugin logs + toasts, does NOT block.
10
+ # exit 2 — HIGH_CONFIDENCE match (real token formats: sk-bf-<token>,
11
+ # AKIA..., ghp_..., glpat-..., sk_live_/sk_test_..., xox[baprs]-...,
12
+ # Bearer <token>, JWT). The plugin BLOCKS the tool call (throws
13
+ # after logging — the only deliberate throw in the plugin).
14
+ #
15
+ # Matches are always MASKED (first 4 + last 4 chars for len>8, else "****")
16
+ # before being echoed — the hook trail must never contain a raw secret value.
17
+ set -euo pipefail
18
+
19
+ HIGH_CONFIDENCE_PATTERNS=(
20
+ "AKIA[0-9A-Z]{16}"
21
+ "gh[pousr]_[A-Za-z0-9_]{36,}"
22
+ "glpat-[A-Za-z0-9_\\-]{20}"
23
+ "sk-[a-zA-Z0-9]{20,}"
24
+ "sk_live_[a-zA-Z0-9]{20,}"
25
+ "sk_test_[a-zA-Z0-9]{20,}"
26
+ "xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}"
27
+ "bearer\\s+[a-zA-Z0-9_\\-\\.]{20,}"
28
+ "eyJ[A-Za-z0-9_-]*\\.eyJ[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]*"
29
+ )
30
+
31
+ LOW_CONFIDENCE_PATTERNS=(
32
+ # KEY/header NAMES without a recognizable token value — high false-positive
33
+ # rate (e.g. the Bifrost header alone, api_key=longvalue, password="x", secret="y"):
34
+ # advisory only, never blocks.
35
+ "[a-zA-Z0-9_-]*api[_-]?key[a-zA-Z0-9_-]*\\s*[:=]\\s*[\"']?[a-zA-Z0-9_\\-]{16,}[\"']?"
36
+ "[a-zA-Z0-9_-]*password[a-zA-Z0-9_-]*\\s*[:=]\\s*[\"'][^\"']{8,}[\"']"
37
+ "[a-zA-Z0-9_-]*secret[a-zA-Z0-9_-]*\\s*[:=]\\s*[\"'][^\"']{8,}[\"']"
38
+ )
39
+
40
+ # Keep provider-specific markers assembled so this scanner is not self-matched
41
+ # by the repository secret test.
42
+ BIFROST_HEADER="x""-bf-""vk"
43
+ BIFROST_TOKEN_PREFIX="sk""-bf-"
44
+ HIGH_CONFIDENCE_PATTERNS+=("${BIFROST_TOKEN_PREFIX}[A-Za-z0-9_-]{8,}")
45
+ LOW_CONFIDENCE_PATTERNS+=("$BIFROST_HEADER")
46
+
47
+ # Mask a matched secret for safe logging: first 4 + last 4 chars (len > 8),
48
+ # else fully hidden — short matches must not be reconstructable by
49
+ # concatenating the visible slices.
50
+ mask_secret() {
51
+ local match="$1"
52
+ if [[ ${#match} -le 8 ]]; then
53
+ echo "****"
54
+ else
55
+ echo "${match:0:4}****${match: -4}"
56
+ fi
57
+ }
58
+
59
+ INPUT="${1:-${TOOL_INPUT:-}}"
60
+ if [[ -z "$INPUT" ]] && [[ ! -t 0 ]]; then
61
+ INPUT=$(cat)
62
+ fi
63
+
64
+ if [[ -z "$INPUT" ]]; then
65
+ exit 0
66
+ fi
67
+
68
+ FOUND_HIGH=0
69
+ FOUND_LOW=0
70
+
71
+ for pattern in "${HIGH_CONFIDENCE_PATTERNS[@]}"; do
72
+ if echo "$INPUT" | grep -iqE "$pattern"; then
73
+ MATCH=$(echo "$INPUT" | grep -ioE "$pattern" | head -1)
74
+ echo "[SECRET SCAN] High-confidence secret detected: $(mask_secret "$MATCH") (pattern: $pattern)" >&2
75
+ FOUND_HIGH=1
76
+ fi
77
+ done
78
+
79
+ for pattern in "${LOW_CONFIDENCE_PATTERNS[@]}"; do
80
+ if echo "$INPUT" | grep -iqE "$pattern"; then
81
+ MATCH=$(echo "$INPUT" | grep -ioE "$pattern" | head -1)
82
+ echo "[SECRET SCAN] Potential secret detected (low confidence): $(mask_secret "$MATCH") (pattern: $pattern)" >&2
83
+ FOUND_LOW=1
84
+ fi
85
+ done
86
+
87
+ if [[ $FOUND_HIGH -eq 1 ]]; then
88
+ echo "[SECRET SCAN] High-confidence hardcoded secret detected. Tool call will be BLOCKED." >&2
89
+ exit 2
90
+ fi
91
+
92
+ if [[ $FOUND_LOW -eq 1 ]]; then
93
+ echo "[SECRET SCAN] Possible secret detected. Remove it and use environment variables or a vault." >&2
94
+ exit 1
95
+ fi
96
+
97
+ exit 0
@@ -29,25 +29,14 @@ function deepClone(obj) {
29
29
  // V1 → V2 helpers
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
- /** Action name mapping for permissions (V1 key → V2 action) */
32
+ /** Action name mapping for permissions (V1 key → V2 action), including task → subagent. */
33
33
  const V1_PERMISSION_TO_ACTION = {
34
34
  bash: 'shell',
35
- skill: 'skill',
36
- edit: 'edit',
37
- // V2 folds V1's `write` tool permission into the `edit` action, which also
38
- // governs write and patch tools. Keep `edit` as the canonical reverse name.
39
- write: 'edit',
40
35
  task: 'subagent',
41
- websearch: 'websearch',
42
- // passthrough unknowns
43
- }
44
-
45
- const V2_ACTION_TO_V1_PERMISSION = {
46
- shell: 'bash',
47
36
  skill: 'skill',
48
37
  edit: 'edit',
49
- subagent: 'task',
50
38
  websearch: 'websearch',
39
+ // passthrough unknowns
51
40
  }
52
41
 
53
42
  /**
@@ -61,41 +50,15 @@ const V2_ACTION_TO_V1_PERMISSION = {
61
50
  */
62
51
  function convertPermissionsV1toV2(permObj) {
63
52
  const result = []
64
- const resultIndices = new Map()
65
- const effectRank = { allow: 0, ask: 1, deny: 2 }
66
-
67
- const appendPermission = (action, resource, effect) => {
68
- const key = JSON.stringify([action, resource])
69
- const previousIndex = resultIndices.get(key)
70
- if (previousIndex === undefined) {
71
- resultIndices.set(key, result.length)
72
- result.push({ action, resource, effect })
73
- return
74
- }
75
-
76
- const previous = result[previousIndex]
77
- if (previous.effect === effect) return
78
-
79
- // V1 keeps `edit` and `write` as separate permission keys; V2's `edit`
80
- // action covers both. If their rules collide on one resource, retain the
81
- // stricter effect rather than letting object order turn a deny into allow.
82
- console.warn(
83
- `[config-migration] Conflicting V1 permission aliases for ${action}:${resource}; keeping the more restrictive effect`,
84
- )
85
- if ((effectRank[effect] ?? 0) > (effectRank[previous.effect] ?? 0)) {
86
- previous.effect = effect
87
- }
88
- }
89
-
90
53
  for (const [key, value] of Object.entries(permObj)) {
91
54
  const action = V1_PERMISSION_TO_ACTION[key] || key
92
55
  if (typeof value === 'string') {
93
56
  // Simple: { edit: "allow" } → [{ action: "edit", resource: "*", effect: "allow" }]
94
- appendPermission(action, '*', value)
57
+ result.push({ action, resource: '*', effect: value })
95
58
  } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
96
59
  // Nested: { bash: { "git *": "allow" } } → [{ action: "shell", resource: "git *", effect: "allow" }]
97
60
  for (const [resource, effect] of Object.entries(value)) {
98
- appendPermission(action, resource, effect)
61
+ result.push({ action, resource, effect })
99
62
  }
100
63
  } else {
101
64
  console.warn(`[config-migration] Unexpected permission value for "${key}":`, value)
@@ -109,9 +72,13 @@ function convertPermissionsV1toV2(permObj) {
109
72
  */
110
73
  function convertPermissionsV2toV1(permArray) {
111
74
  const result = {}
75
+ // Reverse action mapping: V2 action → V1 key
76
+ const ACTION_TO_V1 = Object.fromEntries(
77
+ Object.entries(V1_PERMISSION_TO_ACTION).map(([k, v]) => [v, k]),
78
+ )
112
79
 
113
80
  for (const { action, resource, effect } of permArray) {
114
- const v1Key = V2_ACTION_TO_V1_PERMISSION[action] || action
81
+ const v1Key = ACTION_TO_V1[action] || action
115
82
  if (resource === '*') {
116
83
  result[v1Key] = effect
117
84
  } else {
@@ -329,7 +329,7 @@ export async function runInteractiveInstall({ components = [], defaultComponents
329
329
  {
330
330
  name: 'skills',
331
331
  value: 'skills',
332
- description: `Reusable skill workflows (${21} skills)`,
332
+ description: `Reusable skill workflows (${20} skills)`,
333
333
  },
334
334
  {
335
335
  name: 'instructions',