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

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/CHANGELOG.md CHANGED
@@ -18,6 +18,138 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
18
18
 
19
19
  ## ✅ Closed Issues
20
20
 
21
+ ## [v1.6.0-beta.3] - 2026-10-03
22
+
23
+ <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
24
+ move this section to a versioned entry and reset the template below. -->
25
+
26
+ ## 🆕 What's New
27
+ - **Três ferramentas V2 agora executam de verdade**: `hashline_edit`,
28
+ `pantheon_cost` e `pantheon_model` rodam no host `opencode v2.0.x`. Antes
29
+ eram placeholders que declaravam sucesso sem fazer nada.
30
+ - **Enforcement de sessão read-only no caminho V2**: `apollo` e `gaia` voltam
31
+ a ter escrita bloqueada, com o guard registrado no próprio plugin V2 — sem
32
+ depender de uma ponte que a instalação V2 não monta.
33
+ - **Varredura de segredos de alta confiança no caminho V2**: o único hard block
34
+ de segurança que a V1 tinha, e que estava inerte por viver num plugin que a
35
+ instalação V2 não carrega, passa a rodar.
36
+
37
+ ## 🐞 Fixed
38
+ - **As 6 ferramentas de plugin do V2 falhavam em 100% das chamadas**:
39
+ `hashline_edit`, `pantheon_cost` e `pantheon_model` lançavam
40
+ `Tool result declared output without an output schema` em toda invocação. A
41
+ causa era assimetria local — `draft.add()` não declarava `output` enquanto o
42
+ `execute` resolvia `{ output: string }`. O host exige simetria nos dois
43
+ sentidos: resultado com `output` e nenhuma declaração lança, assim como
44
+ declaração sem `output` no resultado. `output` passou a ser **obrigatório** na
45
+ interface `V2ToolDef`, então o compilador exige a declaração daqui em diante.
46
+ - **A suíte exigia o defeito**: `tests/pantheon/plugin-v2-contract.test.ts`
47
+ afirmava que o resultado carregava `.output` string — literalmente a condição
48
+ de falha, contra um mock que não reproduzia a exigência do host. O teste foi
49
+ reescrito para o invariante de simetria, contra um mock que constrói o
50
+ `outputSchema` a partir de `def.output` como o host faz
51
+ (`output: I.outputSchema ?? {}`), cobrindo as duas direções do bicondicional.
52
+ - **A imposição read-only não existia na superfície V2**: `createEnforcementGuard`
53
+ só era instanciado em `src/plugin.ts`, que o `opencode.json` da release não
54
+ declara — V2 nunca carrega V1. Com `hashline_edit` funcional, uma sessão
55
+ somente-leitura podia escrever arquivos. O guard passou a ser registrado no
56
+ hook `execute.before` do V2, e `pantheon_model` — que escreve
57
+ `active-preset.json` em escopo project e global — foi acrescentado a
58
+ `DEFAULT_BLOCKED_TOOLS`, o que corrige também a V1. `pantheon_cost` ficou de
59
+ fora de propósito: só lê `opencode.db`.
60
+ - **O único hard block de segurança da V1 estava inerte**: o scan de segredos de
61
+ alta confiança vivia em `src/plugins/pantheon-hooks.ts`, um plugin que a
62
+ instalação V2 não carrega — inerte por construção, não por má configuração.
63
+ Passou a rodar dentro do `execute.before` do V2
64
+ (`scripts/hooks/scan-secrets.sh`; exit 2 bloqueia, exit 1 é advisory e só
65
+ registra). O caminho do script foi verificado a partir do tarball instalado,
66
+ não só do repo.
67
+ - **Cinco comentários afirmavam cobertura que não existe** ("wired through the
68
+ V1…"). Corrigidos — é o defeito "anunciado como vivo, morto no caminho", a
69
+ mesma classe do bug original, transferida do tool para o controle de segurança.
70
+
71
+ ## ⚠️ Known Issues
72
+ - **A release entrega 3 ferramentas V2 funcionais, não 6**:
73
+ `pantheon_goal_create`, `pantheon_goal_get` e `pantheon_goal_update` foram
74
+ **removidos** da superfície V2. `GoalLoopDeps` exige `store`, `client` e
75
+ `board`, que não existem no `PluginContext` V2, e a bridge V1 resolve para
76
+ `null` fora da V1. Registrá-las como placeholders não funcionais seria pior
77
+ que a ausência: anunciá-las seria mentira silenciosa. Marcador `goal-tools`
78
+ em `V2_UNSUPPORTED_FEATURES`. **Breaking** para quem usava esses tools no
79
+ host V2 — o contrato V1 permanece disponível para o goal loop.
80
+ - **A matriz de delegação nativa da V1 não é replicada na V2**: o guard V2 cobre
81
+ a lista de ferramentas bloqueadas, mas não a matriz caller→target nem a negação
82
+ de `task` em sessão filha. A matriz precisa de uma hierarquia de sessões
83
+ semeada a partir dos metadados de sessão, e o V2 não expõe esse caminho de
84
+ semeadura — `SessionHierarchyRegistry.isRoot` reporta `true` para sessões
85
+ desconhecidas enquanto não semeada, então semeá-la pela metade negaria todo
86
+ `task()`. O branch é pulado em vez de negado indiscriminadamente. Marcador
87
+ `delegation-matrix`. `task` continua negada em sessão somente-leitura pela
88
+ lista de ferramentas bloqueadas, então a profundidade 2 se sustenta; o que
89
+ não existe é a matriz. Coberto por `experimental.subagent_depth` nesta
90
+ configuração, mas a garantia do plugin não existe.
91
+ - **O log do scan de segredos escreve só em `.pantheon/logs/hooks.log`**: o
92
+ `PluginContext` V2 não expõe `client` nem `directory`, então os canais de log
93
+ estruturado e de toast da V1 não existem nessa superfície. É menos
94
+ observabilidade, não menos bloqueio.
95
+ - **Falha de infraestrutura do scanner é fail-open por decisão**: script ausente
96
+ ou timeout não bloqueiam. `runHook` nunca rejeita e um erro de infraestrutura
97
+ não deve negar chamadas não relacionadas. Ausência do script não bloqueia.
98
+
99
+ ## ✅ Closed Issues
100
+ - **Drift de documentação no `README.pt-BR.md`**: a tabela de contrato de
101
+ runtime e o parágrafo de superfície listavam 6 ferramentas V2 e as 3 de goal,
102
+ que não estão registradas. Corrigido para 3 ferramentas, com a razão da
103
+ ausência e o ponto de imposição read-only.
104
+ - **Cinco comentários de falsa cobertura** sobre a imposição read-only e o scan
105
+ de segredos.
106
+ - **Shim do loader JSON do ajv elevado a bootstrap de módulo**: o patch de
107
+ `Module._extensions['.json']` era aplicado de dentro de `loadAjv()` e ficava
108
+ vivo durante todo o run, podendo vazar entre arquivos de teste e se manifestar
109
+ longe da causa. Agora instala, carrega e restaura em nível de módulo, antes de
110
+ qualquer corpo de teste, com `finally`.
111
+ ## [v1.6.0-beta.2] - 2026-10-02
112
+
113
+ ## 🆕 What's New
114
+ - **`steps` não é mais definido pelo Pantheon**: removido dos 14 agentes em
115
+ `src/agents/`. No OpenCode 2 o campo é opcional e **sem default** — ausente
116
+ significa **sem teto nativo**. O host não impõe teto; o Pantheon estava
117
+ escrevendo um.
118
+ - **Instalador v2 remove `steps` residual**: configs já instalados passam a ter
119
+ o `steps` apagado dos agentes gerenciados. Escopo restrito aos agentes que o
120
+ Pantheon gerencia — um agente definido pelo usuário nunca é tocado. O bloco V1
121
+ singular `agent` é deliberadamente intocado (V1 em retirada).
122
+ - **Substitutos ao teto**: `task(background=true)` para trabalho fora da
123
+ foreground, `experimental.subagent_depth` para aninhamento de subagentes e
124
+ paralelismo entre delegações independentes.
125
+ - **Limpeza**: `max_steps` e `max_compaction_items` removidos de
126
+ `src/routing.yml`. `loadRoutingMaxSteps` nunca teve um único import — 2
127
+ matches no repo (definição e typedef), ambos removidos.
128
+
129
+ ## 🐞 Fixed
130
+ - **`migrateV1toV2` descartava campos gerenciados**: com os blocos `agent` (V1)
131
+ e `agents` (V2) coexistindo — o caso de qualquer config já instalado — o
132
+ spread raso fazia um substituir o outro por inteiro. Na ordem de chaves do
133
+ `opencode.json` real, o lado perdedor era o do usuário. Agora **mescla**: a
134
+ base é o bloco V2, os campos gerenciados do V1 sobrescrevem, e os campos do
135
+ usuário são preservados. Mesma semântica já usada em `install/opencode.mjs`.
136
+ - **`docs/platforms/README.md` tinha tabela de step limits errada**: 14 linhas
137
+ listando limites por agente, **nenhum valor batendo** com `src/agents/`.
138
+ Tabela removida e a seção reescrita.
139
+
140
+ ## ⚠️ Known Issues
141
+ - **Permissões de agente**: V1 usa `permission` e V2 usa `permissions`; ao mesclar esses blocos, a chave legada `permission` pode permanecer inerte ao lado de `permissions`.
142
+ - **Migração — rode o init uma vez**: `npx pantheon-opencode init
143
+ --opencode-version=v2` **uma única vez** limpa o `steps` residual. Sem isso o
144
+ config instalado continua carregando o teto antigo.
145
+ - **O `.md` de agente instalado é sobrescrito no próximo sync**: editar
146
+ `~/.config/opencode/agents/*.md` esperando persistência **não funciona**.
147
+ Comportamento **pré-existente**, não é regressão desta mudança — mas agora
148
+ está documentado para não ser reportado como uma.
149
+ - **`steps` definido à mão num agente gerenciado é removido a cada install v2**:
150
+ o instalador não distingue um valor que ele escreveu de um valor que você
151
+ escreveu (mesma chave, mesmo valor). Workaround: definir no frontmatter do
152
+ agente — e lembrar que ele **também** é sobrescrito no próximo sync.
21
153
  ## [v1.6.0-beta.1] - 2026-09-30
22
154
 
23
155
  <!-- Add new changes here. Running `node scripts/versioning.mjs apply` will
package/README.md CHANGED
@@ -78,11 +78,14 @@ clear handoff between stages of work.
78
78
 
79
79
  ## Status
80
80
 
81
- Operational checkout version: **v1.6.0-beta.1** (first beta compatible with
82
- OpenCode 2; publication is not asserted here). Pantheon is designed for OpenCode and depends on the
83
- availability and configuration of OpenCode and any optional services you choose
84
- to use. Check the [releases](https://github.com/ils15/pantheon-opencode/releases)
85
- and [changelog](CHANGELOG.md) for the latest published changes.
81
+ Candidate version: the manifests in this checkout express the version being
82
+ prepared, not a confirmed publication. A beta is published only after the
83
+ Release workflow completes successfully. To check which version is actually
84
+ published, consult the npm `beta` dist-tag. Pantheon is designed for OpenCode
85
+ and depends on the availability and configuration of OpenCode and any optional
86
+ services you choose to use. Check the
87
+ [releases](https://github.com/ils15/pantheon-opencode/releases) and
88
+ [changelog](CHANGELOG.md) for the latest published changes.
86
89
 
87
90
  ## Delegation (native `task()`)
88
91
 
@@ -183,16 +186,36 @@ per installation; V1 and V2 Pantheon plugins must never be registered together.
183
186
  |---|---|---|
184
187
  | OpenCode config key | singular `plugin` | plural `plugins` |
185
188
  | Pantheon registration | `src/plugin.ts` plus `src/plugins/pantheon-hooks.ts` | `<installed>/src/plugin-v2` directory (`index.ts` re-exports `src/plugin-v2.ts`) |
186
- | Runtime contract | Pantheon V1 plugin: 6 tools (`hashline_edit`, the 3 goal tools, `pantheon_cost`, `pantheon_model`), event/tool hooks and V1 compaction handling | Full V2 plugin: 6 orchestration tools, 4 event subscriptions, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), plus configuration transforms |
189
+ | Runtime contract | Pantheon V1 plugin: 6 tools (`hashline_edit`, the 3 goal tools, `pantheon_cost`, `pantheon_model`), event/tool hooks and V1 compaction handling | Full V2 plugin: 3 tools (`hashline_edit`, `pantheon_cost`, `pantheon_model`), 4 event subscriptions, session hooks (`prompt`, `context`), a read-only-enforcing tool `execute.before` hook, plus configuration transforms |
187
190
  | V1 APIs | Registered | Own tool definitions via `ctx.tool.transform()` — not the V1 plugin path |
188
191
 
189
- The V2 plugin provides 6 orchestration tools (`hashline_edit`,
190
- `pantheon_goal_create`, `pantheon_goal_get`, `pantheon_goal_update`,
191
- `pantheon_cost`, `pantheon_model`), 4 event subscriptions (`session.created`,
192
- `session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
193
- `context`), and tool hooks (`execute.before`, `execute.after`). Unsupported V2
194
- features are `legacy-hooks` (the V1-specific hook surface), `catalog-transform`,
195
- `integration-transform`, and `skill-transform`.
192
+ The V2 plugin provides 3 orchestration tools (`hashline_edit`, `pantheon_cost`,
193
+ `pantheon_model`), 4 event subscriptions (`session.created`, `session.idle`,
194
+ `session.error`, `session.compacted`), session hooks (`prompt`, `context`),
195
+ and a tool `execute.before` hook that enforces read-only sessions. The 3 goal
196
+ tools are **not**
197
+ registered on V2: the goal loop needs a `GoalStore`, a `GoalLoopClient` and a
198
+ `BackgroundJobBoard`, none of which the V2 `PluginContext` exposes, so the gap
199
+ is reported as the `goal-tools` unsupported-feature marker. Use the V1
200
+ contract if you need the goal loop.
201
+
202
+ The V2 `execute.before` hook is the read-only enforcement point for that
203
+ surface: the host includes the active agent on the event, a read-only agent
204
+ (`apollo`, `gaia`) registers its session, and the shared
205
+ `createEnforcementGuard` throws to deny `edit`, `write`, `bash`, `task`,
206
+ `hashline_edit` and `pantheon_model`. It does **not** rely on the V1
207
+ `tool.execute.before` hook, which lives in `src/plugin.ts` and is not loaded
208
+ when only `plugin-v2` is configured. The companion `execute.after` hook and the
209
+ `permission.evaluate` hook are registration points with no V2-side behaviour,
210
+ and the V1 caller/target delegation matrix for native `task()` is not enforced
211
+ on V2.
212
+
213
+ Every V2 tool declares an `output` schema. OpenCode 2.0.x requires the
214
+ declaration and the resolved result to agree in both directions, so an
215
+ undeclared tool — or one that returns `output` without declaring it — fails on
216
+ every call. Unsupported V2 features are `legacy-hooks` (the V1-specific hook
217
+ surface), `catalog-transform`, `integration-transform`, `skill-transform` and
218
+ `goal-tools`.
196
219
 
197
220
  The package exposes both contracts as importable exports: `pantheon-opencode/plugin`
198
221
  (V1), `pantheon-opencode/plugin-v2` (V2) and `pantheon-opencode/v2-bridge`
@@ -231,6 +254,29 @@ The installer still writes the compatibility settings required by the selected
231
254
  OpenCode host, such as `experimental.subagent_depth`; this does not convert a
232
255
  V1 plugin into V2 or provide V2 with V1 hooks.
233
256
 
257
+ Pantheon does not set a step ceiling on agents. On OpenCode 2 the `steps`
258
+ field is optional and has no default: absent means the host applies no native
259
+ limit. A config written before this change still carries the old value, and the
260
+ V2 install path strips it from every agent Pantheon manages — an agent you
261
+ define yourself is never touched, and the retiring V1 singular `agent` block
262
+ is left alone. Run the installer once to clear a stale value from an existing
263
+ config:
264
+
265
+ ```bash
266
+ npx pantheon-opencode init --opencode-version=v2
267
+ ```
268
+
269
+ Context control is not a Pantheon setting. Whatever budget an agent runs
270
+ against comes from the compaction settings of the OpenCode host you are
271
+ running, and Pantheon does not define them.
272
+
273
+ When a V1 `agent` block and a V2 `agents` block are both present — which is
274
+ what an installed `opencode.json` carries — the V1→V2 conversion merges them
275
+ instead of letting one replace the other. The V2 block is the base, the
276
+ framework-managed fields from the V1 block overwrite, and your own fields are
277
+ preserved, matching the precedence the installer already applies elsewhere.
278
+ Key order does not matter.
279
+
234
280
  ## Updating between releases (beta.5+)
235
281
 
236
282
  One command keeps an existing installation current:
package/README.pt-BR.md CHANGED
@@ -122,16 +122,36 @@ registrados juntos.
122
122
  |---|---|---|
123
123
  | Chave de config do OpenCode | `plugin` singular | `plugins` plural |
124
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: 6 ferramentas de orquestração, 4 assinaturas de eventos, session hooks (`prompt`, `context`), tool hooks (`execute.before`/`after`), além de transforms de configuração |
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 |
126
126
  | APIs V1 | Registradas | Definições próprias de ferramentas via `ctx.tool.transform()` — não pelo caminho do plugin V1 |
127
127
 
128
- O plugin V2 fornece 6 ferramentas de orquestração (`hashline_edit`,
129
- `pantheon_goal_create`, `pantheon_goal_get`, `pantheon_goal_update`,
128
+ O plugin V2 fornece 3 ferramentas de orquestração (`hashline_edit`,
130
129
  `pantheon_cost`, `pantheon_model`), 4 assinaturas de eventos (`session.created`,
131
130
  `session.idle`, `session.error`, `session.compacted`), session hooks (`prompt`,
132
- `context`) e tool hooks (`execute.before`, `execute.after`). Os recursos V2 sem
133
- suporte são `legacy-hooks` (a superfície de hooks específica do V1),
134
- `catalog-transform`, `integration-transform` e `skill-transform`.
131
+ `context`) e um hook de ferramenta `execute.before` que impõe sessões
132
+ somente-leitura. As 3 ferramentas de goal **não** são
133
+ registradas no V2: o goal loop precisa de um `GoalStore`, de um
134
+ `GoalLoopClient` e de um `BackgroundJobBoard`, nenhum dos quais o
135
+ `PluginContext` do V2 expõe, então a lacuna é reportada como o marcador de
136
+ recurso sem suporte `goal-tools`. Use o contrato V1 se precisar do goal loop.
137
+
138
+ O hook `execute.before` do V2 é o ponto de imposição de sessões somente-leitura
139
+ dessa superfície: o host inclui o agente ativo no evento, um agente somente-leitura
140
+ (`apollo`, `gaia`) registra sua sessão, e o `createEnforcementGuard`
141
+ compartilhado lança para negar `edit`, `write`, `bash`, `task`,
142
+ `hashline_edit` e `pantheon_model`. Ele **não** depende do hook
143
+ `tool.execute.before` do V1, que vive em `src/plugin.ts` e não é carregado
144
+ quando apenas `plugin-v2` está configurado. Os hooks companheiros
145
+ `execute.after` e `permission.evaluate` são pontos de registro sem comportamento
146
+ do lado V2, e a matriz de delegação caller/target da V1 para o `task()` nativo
147
+ não é imposta no V2.
148
+
149
+ Toda ferramenta V2 declara um schema `output`. O OpenCode 2.0.x exige que a
150
+ declaração e o resultado resolvido concordem nos dois sentidos, então uma
151
+ ferramenta sem declaração — ou que retorne `output` sem declará-lo — falha em
152
+ toda chamada. Os recursos V2 sem suporte são `legacy-hooks` (a superfície de
153
+ hooks específica do V1), `catalog-transform`, `integration-transform`,
154
+ `skill-transform` e `goal-tools`.
135
155
 
136
156
  O pacote expõe os dois contratos como exports importáveis:
137
157
  `pantheon-opencode/plugin` (V1), `pantheon-opencode/plugin-v2` (V2) e
@@ -171,6 +191,29 @@ O instalador continua gravando as configurações de compatibilidade exigidas
171
191
  pelo host OpenCode selecionado, como `experimental.subagent_depth`; isso não
172
192
  converte um plugin V1 em V2 nem dá hooks V1 ao V2.
173
193
 
194
+ O Pantheon não define mais um teto de passos para os agentes. No OpenCode 2 o
195
+ campo `steps` é opcional e não tem default: ausente significa que o host não
196
+ aplica nenhum limite nativo. Uma config escrita antes dessa mudança ainda
197
+ carrega o valor antigo, e o caminho de instalação v2 o remove de todos os
198
+ agentes que o Pantheon gerencia — um agente definido por você nunca é tocado,
199
+ e o bloco V1 singular `agent`, em retirada, é deixado intacto. Rode o
200
+ instalador uma vez para limpar um valor antigo de uma config existente:
201
+
202
+ ```bash
203
+ npx pantheon-opencode init --opencode-version=v2
204
+ ```
205
+
206
+ O controle de contexto não é uma configuração do Pantheon. Qualquer orçamento
207
+ que um agente terá vem das settings de compactação do próprio host OpenCode,
208
+ e o Pantheon não as define.
209
+
210
+ Quando um bloco V1 `agent` e um bloco V2 `agents` coexistem — o que acontece
211
+ em qualquer `opencode.json` já instalado — a conversão V1→V2 mescla os dois em
212
+ vez de deixar um substituir o outro. O bloco V2 é a base, os campos
213
+ gerenciados que vêm do V1 sobrescrevem, e os seus próprios campos são
214
+ preservados, com a mesma precedência que o instalador já aplica em outros
215
+ pontos. A ordem das chaves não muda o resultado.
216
+
174
217
  ## Atualizando entre releases (beta.5+)
175
218
 
176
219
  Um comando mantém uma instalação existente em dia:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantheon-opencode",
3
- "version": "1.6.0-beta.1",
3
+ "version": "1.6.0-beta.3",
4
4
  "description": "Multi-agent orchestration for OpenCode — 14 specialized agents, TDD, Themis review gates.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -105,6 +105,7 @@
105
105
  "@biomejs/biome": "2.5.13",
106
106
  "@commitlint/cli": "21.2.2",
107
107
  "@commitlint/config-conventional": "21.2.2",
108
+ "ajv": "8.20.0",
108
109
  "husky": "^9.1.7",
109
110
  "tsx": "4.23.13",
110
111
  "typescript": "6.0.3"
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pantheon",
3
3
  "publisher": "ils15",
4
- "version": "1.6.0-beta.1",
4
+ "version": "1.6.0-beta.3",
5
5
  "displayName": "Pantheon",
6
6
  "description": "Conductor-Delegate orchestration platform with 14 specialized agents, 21 skills, and OpenCode support.",
7
7
  "agents": "./src/agents",
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.1"
7
+ version = "1.6.0-beta.3"
8
8
  description = "Multi-agent orchestration framework"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = []
@@ -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)) {
@@ -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`
package/skills-lock.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "src/skills/agent-coordination/SKILL.md": "cfd040ef29e3dd0fd5e0e67773874facf7c06e3284898afe1fddba4456c3f1aa",
3
3
  "src/skills/artifact-management/SKILL.md": "c60e493666bedf46e9113d0dd20106489d5c5a73905e1370fcfc1c0d71c59185",
4
- "src/skills/auto-continue/SKILL.md": "f6ff530b5537af9c8a0224a52eb0da6da7f9cf4ab4958ce2908c5c3a911b8879",
4
+ "src/skills/auto-continue/SKILL.md": "4fe256d44ee9828d622a0d8b63cfc6bb39a317189a9ccf19494a6efd4527a7a9",
5
5
  "src/skills/clonedeps/SKILL.md": "3e0b43cf96da98c83699dc48db3fd58fbfda84b9022bb70d3747875c81c3e124",
6
6
  "src/skills/code-review-checklist/SKILL.md": "0e59367c65ef4608db80a0674279f32cfc3f67dca7236f233b61a47fe506579f",
7
7
  "src/skills/codemap/SKILL.md": "87af5271e1576e154cadff313eab16264b726e43d094520736b7a5b03b409a4a",
@@ -14,7 +14,6 @@ permission:
14
14
  task:
15
15
  "*": deny
16
16
  temperature: 0.3
17
- steps: 70
18
17
  skills:
19
18
  - tdd-with-agents
20
19
  - visual-review-pipeline
@@ -17,7 +17,6 @@ permission:
17
17
  task:
18
18
  "*": deny
19
19
  temperature: 0.1
20
- steps: 30
21
20
  skills:
22
21
  - auto-continue
23
22
  mcp_tools:
@@ -6,7 +6,6 @@ description: Strategic planner & architect — research-first, plan-only, never
6
6
  mode: all
7
7
  reasoning_effort: high
8
8
 
9
- steps: 30
10
9
  mcp_tools:
11
10
  pantheon-resources: all
12
11
  pantheon-memory: [memory_search]
@@ -12,7 +12,6 @@ permission:
12
12
  task:
13
13
  "*": deny
14
14
  temperature: 0.2
15
- steps: 60
16
15
  skills:
17
16
  - tdd-with-agents
18
17
  - incremental-implementation
@@ -16,7 +16,6 @@ permission:
16
16
  task:
17
17
  "*": deny
18
18
  temperature: 0.2
19
- steps: 30
20
19
  skills:
21
20
  - auto-continue
22
21
  mcp_tools:
@@ -12,7 +12,6 @@ permission:
12
12
  task:
13
13
  "*": deny
14
14
  temperature: 0.3
15
- steps: 60
16
15
  skills:
17
16
  - tdd-with-agents
18
17
  - auto-continue
@@ -11,7 +11,6 @@ mcp_tools:
11
11
  pantheon-memory: [memory_search]
12
12
  pantheon-code-mode: [execute_code_script]
13
13
  temperature: 0.3
14
- steps: 70
15
14
  skills:
16
15
  - tdd-with-agents
17
16
  - file-prompts
@@ -14,7 +14,6 @@ permission:
14
14
  task:
15
15
  "*": deny
16
16
  temperature: 0.2
17
- steps: 25
18
17
  skills:
19
18
  - git-workflow-and-versioning
20
19
  - artifact-management
@@ -6,7 +6,6 @@ description: Memory bank quality owner — initializes .pantheon/memory-bank/, w
6
6
  mode: all
7
7
  reasoning_effort: low
8
8
 
9
- steps: 25
10
9
  mcp_tools:
11
10
  pantheon-resources: all
12
11
  pantheon-memory: