spec-first-copilot 0.5.0-beta.2 → 0.5.0-beta.4
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/lib/init.js +2 -2
- package/package.json +1 -1
- package/templates/.github/adapters/confluence.md +273 -0
- package/templates/.github/adapters/errors.md +234 -0
- package/templates/.github/adapters/filesystem.md +353 -0
- package/templates/.github/adapters/interface.md +301 -0
- package/templates/.github/adapters/naming.md +241 -0
- package/templates/.github/adapters/registry.md +244 -0
- package/templates/.github/copilot-instructions.md +4 -5
- package/templates/.github/skills/sf-new-project/SKILL.md +128 -0
- package/templates/sfw.config.yml.example +131 -0
- package/templates/.github/skills/sf-setup-projeto/SKILL.md +0 -123
- package/templates/workspace/Input/setup_projeto/.gitkeep +0 -0
package/lib/init.js
CHANGED
|
@@ -62,8 +62,8 @@ function init({ name, templatesDir, targetDir }) {
|
|
|
62
62
|
console.log(`\nDone! Project "${name}" is ready.`);
|
|
63
63
|
console.log('\nNext steps:');
|
|
64
64
|
console.log(` 1. cd ${name}`);
|
|
65
|
-
console.log(' 2.
|
|
66
|
-
console.log(' 3. Run /sf-
|
|
65
|
+
console.log(' 2. Create a folder in workspace/Input/ with your project files (e.g. workspace/Input/my_app/)');
|
|
66
|
+
console.log(' 3. Run /sf-new-project <folder-name> to start the pipeline');
|
|
67
67
|
console.log('');
|
|
68
68
|
}
|
|
69
69
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# ConfluenceAdapter — Runbook
|
|
2
|
+
|
|
3
|
+
> Implementação do `SourceAdapter` para Atlassian Confluence via MCP
|
|
4
|
+
> `sooperset/mcp-atlassian` (validado end-to-end em 2026-04-11 —
|
|
5
|
+
> ver `planodetarefas.md §9.9` e napkin "Hard findings do teste E2E").
|
|
6
|
+
>
|
|
7
|
+
> Este arquivo é o **runbook que o agent segue** quando uma skill precisa
|
|
8
|
+
> de uma operação de Confluence. A interface conceitual está em
|
|
9
|
+
> `.github/adapters/interface.md`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Meta
|
|
14
|
+
|
|
15
|
+
| Campo | Valor |
|
|
16
|
+
|-------|-------|
|
|
17
|
+
| `name` | `confluence` |
|
|
18
|
+
| MCP provider | `sooperset/mcp-atlassian` via uvx |
|
|
19
|
+
| Credenciais | `.mcp.json` gitignored (hardcoded, `${VAR}` **não funciona**) |
|
|
20
|
+
| Transporte | MCP — tools prefixadas `mcp__atlassian__confluence_*` |
|
|
21
|
+
| Conflict detection | **Client-side obrigatório** (MCP não aceita `expectedVersion`) |
|
|
22
|
+
| Suporta attachments? | ✅ leitura + escrita |
|
|
23
|
+
| Suporta busca? | Fora da interface (skills podem chamar `confluence_search` direto se precisar) |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Config
|
|
28
|
+
|
|
29
|
+
Passada via `sfw.config.yml > input.config` ou `output.targets[].config`.
|
|
30
|
+
|
|
31
|
+
| Campo | Obrig. | Tipo | Descrição |
|
|
32
|
+
|-------|:---:|------|-----------|
|
|
33
|
+
| `space_key` | ✅ | string | Ex: `"ST"`. Usado em todas as operações que exigem space. |
|
|
34
|
+
| `parent_page_id` | ✅ | string | ID numérico (como string) da página raiz. Ex: `"360668"`. |
|
|
35
|
+
| `recursive` | — | boolean | Default `true`. Se `false`, `listChildren` não desce. |
|
|
36
|
+
| `include_attachments` | — | boolean | Default `true`. Se `false`, `fetchAttachments` retorna `[]`. |
|
|
37
|
+
|
|
38
|
+
### `validateConfig`
|
|
39
|
+
|
|
40
|
+
Lança `ValidationError` se:
|
|
41
|
+
- `space_key` ausente, vazio ou não-string
|
|
42
|
+
- `parent_page_id` ausente, vazio ou não parseável como inteiro
|
|
43
|
+
- `recursive` presente mas não-boolean
|
|
44
|
+
- `include_attachments` presente mas não-boolean
|
|
45
|
+
|
|
46
|
+
Não valida que `space_key` existe no Confluence — isso acontece no primeiro
|
|
47
|
+
`listChildren` e vira `AuthError` ou `NotFoundError`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Mapeamento da interface → MCP
|
|
52
|
+
|
|
53
|
+
### `listChildren(containerId) → Item[]`
|
|
54
|
+
|
|
55
|
+
**MCP**: `mcp__atlassian__confluence_get_page_children`
|
|
56
|
+
|
|
57
|
+
**Passos**:
|
|
58
|
+
1. Chamar tool com `parent_id: containerId`, `limit: 100`, `expand: "version"`
|
|
59
|
+
2. Para cada filho retornado, montar `Item`:
|
|
60
|
+
- `id` = `page.id` (string)
|
|
61
|
+
- `title` = `page.title`
|
|
62
|
+
- `version` = `String(page.version.number)`
|
|
63
|
+
- `type` = `"page"` (Confluence só tem páginas — folders são páginas vazias convencionadas)
|
|
64
|
+
- `hasChildren` = `page.children_count > 0` (se vier na resposta; senão `true` por default — custo de 1 chamada extra é baixo)
|
|
65
|
+
3. Se retorno paginado (cursor), iterar até esgotar
|
|
66
|
+
4. Retornar array
|
|
67
|
+
|
|
68
|
+
**Erros**:
|
|
69
|
+
- `404` → `NotFoundError { kind: "container", itemId: containerId }`
|
|
70
|
+
- `401/403` → `AuthError`
|
|
71
|
+
- `5xx`/timeout → `TransportError { retryable: true }`
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### `fetchContent(itemId) → { content, metadata }`
|
|
76
|
+
|
|
77
|
+
**MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: true`
|
|
78
|
+
|
|
79
|
+
**Passos**:
|
|
80
|
+
1. Chamar tool com `page_id: itemId`, `convert_to_markdown: true`, `include_metadata: true`
|
|
81
|
+
2. Extrair `content` do campo markdown retornado
|
|
82
|
+
3. Montar `metadata`:
|
|
83
|
+
- `version` = `page.version.number` (inteiro)
|
|
84
|
+
- `space_key`, `parent_id`, `created_at`, `updated_at`, `author`
|
|
85
|
+
- `labels` = array das labels da página (usado pelo caller pra checar `sfw-approved`)
|
|
86
|
+
4. Retornar `{ content, metadata }`
|
|
87
|
+
|
|
88
|
+
**Notas importantes**:
|
|
89
|
+
- Markdown roundtrip **normaliza** (`#` vira `===` no H1, `-` vira `*` em listas).
|
|
90
|
+
`version.number` é a fonte de verdade pra drift, **não** diff de bytes.
|
|
91
|
+
- `labels` vêm via `confluence_get_labels` **separadamente** se não vierem
|
|
92
|
+
embutidos na resposta do `get_page`. Chamar só se o caller pediu (lazy).
|
|
93
|
+
|
|
94
|
+
**Erros**: mesmas regras do `listChildren`.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### `fetchAttachments(itemId) → Attachment[]`
|
|
99
|
+
|
|
100
|
+
**MCP**:
|
|
101
|
+
- `mcp__atlassian__confluence_get_attachments` (lista)
|
|
102
|
+
- `mcp__atlassian__confluence_download_attachment` (bytes, lazy)
|
|
103
|
+
|
|
104
|
+
**Passos**:
|
|
105
|
+
1. Se `config.include_attachments === false`, retornar `[]` sem chamar MCP.
|
|
106
|
+
2. Chamar `get_attachments` com `page_id: itemId`
|
|
107
|
+
3. Para cada item retornado, montar `Attachment`:
|
|
108
|
+
- `filename` = `attachment.title`
|
|
109
|
+
- `size` = `attachment.size` em bytes
|
|
110
|
+
- `mimeType` = `attachment.mime_type`
|
|
111
|
+
- `download()` = closure que chama `confluence_download_attachment` com
|
|
112
|
+
`page_id: itemId, filename: attachment.title` e retorna `Buffer`
|
|
113
|
+
4. Retornar array
|
|
114
|
+
|
|
115
|
+
**Lazy**: `download()` só dispara quando o caller chama. Listar attachments é
|
|
116
|
+
barato, baixar PDFs pode não ser.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### `getVersion(itemId) → string`
|
|
121
|
+
|
|
122
|
+
**MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: false`
|
|
123
|
+
|
|
124
|
+
**Passos**:
|
|
125
|
+
1. Chamar tool com `page_id: itemId`, `include_metadata: true`, `convert_to_markdown: false`
|
|
126
|
+
(evita o custo de converter body quando só queremos version)
|
|
127
|
+
2. Retornar `String(page.version.number)`
|
|
128
|
+
|
|
129
|
+
**Nota**: se o MCP oferecer um `confluence_get_page_history` mais barato no
|
|
130
|
+
futuro, trocar. Hoje `get_page` é o caminho mais direto. Custo: ~1 request HTTP.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### `create(parentId, title, content) → { itemId, version }`
|
|
135
|
+
|
|
136
|
+
**MCP**: `mcp__atlassian__confluence_create_page`
|
|
137
|
+
|
|
138
|
+
**Passos**:
|
|
139
|
+
1. Chamar tool com:
|
|
140
|
+
- `space_key: config.space_key`
|
|
141
|
+
- `parent_id: parentId`
|
|
142
|
+
- `title: title` (já vem formatado pelo naming engine — ex: `"app_barbearia - TRD"`)
|
|
143
|
+
- `content: content` (markdown — MCP converte pra storage format)
|
|
144
|
+
- `content_format: "markdown"`
|
|
145
|
+
2. Retornar:
|
|
146
|
+
- `itemId` = `response.page.id`
|
|
147
|
+
- `version` = `String(response.page.version.number)` (tipicamente `"1"`)
|
|
148
|
+
|
|
149
|
+
**Erros específicos**:
|
|
150
|
+
- `BadRequestException: A page already exists with the same TITLE in this space` →
|
|
151
|
+
`ConflictError { kind: "duplicate_title", itemId: null }`
|
|
152
|
+
(naming templating com `{scope}` no título deveria impedir isso — se bateu,
|
|
153
|
+
dois scopes têm o mesmo nome, o user precisa renomear no Input)
|
|
154
|
+
- `parent_id` inválido → `NotFoundError { kind: "container", itemId: parentId }`
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### `update(itemId, content, expectedVersion) → { version, ok, conflict? }`
|
|
159
|
+
|
|
160
|
+
**MCP**: `mcp__atlassian__confluence_update_page` (+ `get_page` pra version check)
|
|
161
|
+
|
|
162
|
+
> **Atenção crítica**: o MCP sooperset **não** aceita `expectedVersion` como
|
|
163
|
+
> parâmetro. Isso significa **zero optimistic locking server-side**. Toda a
|
|
164
|
+
> conflict detection é client-side — responsabilidade deste adapter.
|
|
165
|
+
|
|
166
|
+
**Passos**:
|
|
167
|
+
1. Chamar `getVersion(itemId)` → `currentVersion`
|
|
168
|
+
- Se o item sumiu → `NotFoundError`
|
|
169
|
+
2. Comparar `currentVersion !== expectedVersion`:
|
|
170
|
+
- **Se diverge** → retornar `{ version: currentVersion, ok: false, conflict: true }`
|
|
171
|
+
(sem chamar `update_page` — não sobrescreve drift)
|
|
172
|
+
3. Se bate, chamar `confluence_update_page` com:
|
|
173
|
+
- `page_id: itemId`
|
|
174
|
+
- `title: <mesmo título atual>` (pegar do `get_page` anterior se não vier em cache)
|
|
175
|
+
- `content: content`
|
|
176
|
+
- `content_format: "markdown"`
|
|
177
|
+
- `version_comment: "sfw: auto-publish"` (opcional mas recomendado pra histórico Confluence)
|
|
178
|
+
4. Retornar `{ version: String(response.page.version.number), ok: true }`
|
|
179
|
+
|
|
180
|
+
**Race window**: há uma pequena janela entre o `getVersion` e o `update_page`
|
|
181
|
+
onde outra pessoa pode ter editado. MVP aceita — o caso é raro e o próximo
|
|
182
|
+
`getVersion`/`update` no ciclo detecta. Se virar dor, documentar como
|
|
183
|
+
limitação conhecida.
|
|
184
|
+
|
|
185
|
+
**Erros específicos**:
|
|
186
|
+
- `version_drift` detectado no passo 2 → retorno `{ ok: false, conflict: true }`
|
|
187
|
+
(não lança erro — é caminho normal do contrato)
|
|
188
|
+
- Qualquer outro erro → embrulhar conforme `errors.md`
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### `attachFile?(itemId, filename, content, mimeType)`
|
|
193
|
+
|
|
194
|
+
**MCP**: `mcp__atlassian__confluence_upload_attachment` (ou `upload_attachments` batch)
|
|
195
|
+
|
|
196
|
+
**Passos**:
|
|
197
|
+
1. Chamar tool com `page_id: itemId`, `filename`, `content` (bytes), `mime_type`
|
|
198
|
+
2. Não retorna valor — sucesso é ausência de erro
|
|
199
|
+
|
|
200
|
+
**Implementação opcional no MVP**: ativar se/quando o `/publish` precisar
|
|
201
|
+
anexar diagramas, planilhas, etc. Por padrão, não chamado.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Constraints descobertos (aprendizado do teste E2E)
|
|
206
|
+
|
|
207
|
+
1. **Title uniqueness por SPACE inteiro** — não por parent.
|
|
208
|
+
**Premissa**: 1 space = 1 projeto. Multi-projeto no mesmo space causa
|
|
209
|
+
colisão se dois scopes tiverem o mesmo nome.
|
|
210
|
+
**Mitigação**: user nomeia items no Input com nomes únicos; naming
|
|
211
|
+
templating com `{scope}` no `output_artifact` garante unicidade no Output.
|
|
212
|
+
|
|
213
|
+
2. **Markdown roundtrip é lossy** — `#` vira `===`, `-` vira `*`, emojis
|
|
214
|
+
podem normalizar. **Consequência**: nunca usar diff de bytes pra detectar
|
|
215
|
+
drift. `version.number` é a única fonte de verdade.
|
|
216
|
+
|
|
217
|
+
3. **`.mcp.json` com `${VAR}` não funciona** no startup do Claude Code.
|
|
218
|
+
Credenciais precisam estar hardcoded no arquivo (gitignored). Mudanças
|
|
219
|
+
exigem reiniciar Claude Code — servers MCP só sobem no startup.
|
|
220
|
+
|
|
221
|
+
4. **`page_id` é chave estável** — renomeação preserva ID, history, children.
|
|
222
|
+
Skills persistem ID em logs, não título.
|
|
223
|
+
|
|
224
|
+
5. **`recursive: true` no `get_page_children`** não é suportado pelo MCP
|
|
225
|
+
atual — adapter precisa fazer recursão manual (loop + stack) quando
|
|
226
|
+
`config.recursive === true`.
|
|
227
|
+
|
|
228
|
+
6. **Paginação**: `get_page_children` retorna `limit` default 25. Caller
|
|
229
|
+
deve iterar via `start` ou cursor pra esgotar filhos — não assumir
|
|
230
|
+
que primeira página é tudo.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Checklist de pré-requisitos (documentar no bootstrap do kit)
|
|
235
|
+
|
|
236
|
+
Antes de usar este adapter, usuário precisa:
|
|
237
|
+
|
|
238
|
+
- [ ] `uvx` instalado (ou `pipx`/`pip`)
|
|
239
|
+
- [ ] `.mcp.json` na raiz do projeto com entrada pro `sooperset/mcp-atlassian`
|
|
240
|
+
+ credenciais hardcoded (NUNCA commitar — está no `.gitignore` do kit)
|
|
241
|
+
- [ ] Token Atlassian com scope de leitura + escrita no space alvo
|
|
242
|
+
- [ ] `space_key` descoberto (ex: via `confluence_search` ou URL do Confluence)
|
|
243
|
+
- [ ] Página raiz do projeto criada (manualmente ou via `/new-project`)
|
|
244
|
+
- [ ] Claude Code reiniciado após qualquer mudança no `.mcp.json`
|
|
245
|
+
|
|
246
|
+
Bootstrap do kit (§9.9.P0.3) automatiza isso.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Matriz de erros Confluence → tipos do SFW
|
|
251
|
+
|
|
252
|
+
| Situação Confluence | Tipo lançado | Detalhes |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| 401 / token inválido | `AuthError` | `hint`: "verifique token em .mcp.json" |
|
|
255
|
+
| 403 / sem permissão | `AuthError` | `hint`: "usuário sem acesso ao space {space_key}" |
|
|
256
|
+
| 404 page_id | `NotFoundError` | `kind: "item"` |
|
|
257
|
+
| 404 parent_id | `NotFoundError` | `kind: "container"` |
|
|
258
|
+
| `BadRequestException` title duplicado | `ConflictError` | `kind: "duplicate_title"` |
|
|
259
|
+
| 5xx Confluence down | `TransportError` | `retryable: true` |
|
|
260
|
+
| MCP server não responde | `TransportError` | `retryable: true`, `hint`: "reinicie Claude Code" |
|
|
261
|
+
| Timeout | `TransportError` | `retryable: true` |
|
|
262
|
+
| Attachment muito grande | `TransportError` | `retryable: false`, `hint`: tamanho máx Confluence |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Referências
|
|
267
|
+
|
|
268
|
+
- Interface: `.github/adapters/interface.md`
|
|
269
|
+
- Erros: `.github/adapters/errors.md`
|
|
270
|
+
- Naming: `.github/adapters/naming.md`
|
|
271
|
+
- Registry: `.github/adapters/registry.md`
|
|
272
|
+
- Validação E2E: `planodetarefas.md §9.9` + napkin "Estado do Confluence MCP"
|
|
273
|
+
- MCP provider: [sooperset/mcp-atlassian](https://github.com/sooperset/mcp-atlassian)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# SourceAdapter — Contrato de Erros
|
|
2
|
+
|
|
3
|
+
> Classes de erro tipadas que qualquer adapter **deve** lançar em vez de
|
|
4
|
+
> `throw new Error(...)`. As skills (`/load`, `/publish`, `/new-project`,
|
|
5
|
+
> `/extract`, `/design`, `/plan`) fazem `catch` tipado e decidem o que fazer.
|
|
6
|
+
>
|
|
7
|
+
> Arquivo é referência conceitual, não runtime. A implementação real vive no
|
|
8
|
+
> adapter específico (`.github/adapters/confluence.md` etc.).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Por que erros tipados
|
|
13
|
+
|
|
14
|
+
1. **Skills reagem diferente por categoria.** Um `ConflictError` é recuperável
|
|
15
|
+
(pede ao usuário), um `AuthError` é fatal (para tudo e orienta setup).
|
|
16
|
+
Genérico obriga a parsear mensagem — frágil.
|
|
17
|
+
2. **Logs ficam úteis.** `.ai/load-log.md` e `.ai/publish-log.md` registram
|
|
18
|
+
o tipo do erro, não a mensagem livre. Re-execução pode filtrar por tipo.
|
|
19
|
+
3. **Testes ficam honestos.** FilesystemAdapter (mock natural) lança os mesmos
|
|
20
|
+
tipos — skills testadas contra ele ganham cobertura real.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Hierarquia
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
SourceAdapterError (base — nunca lançado direto)
|
|
28
|
+
├── ValidationError (config do sfw.config.yml inválido)
|
|
29
|
+
├── AuthError (credenciais inválidas ou ausentes)
|
|
30
|
+
├── NotFoundError (itemId / containerId inexistente)
|
|
31
|
+
├── ConflictError (version drift, título duplicado)
|
|
32
|
+
├── TransportError (rede, MCP down, timeout)
|
|
33
|
+
└── NotImplementedError (método opcional não implementado)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Todos os erros **devem** carregar:
|
|
37
|
+
- `name` — nome da classe (ex: `"ConflictError"`)
|
|
38
|
+
- `adapter` — nome do adapter que lançou (ex: `"confluence"`)
|
|
39
|
+
- `operation` — método que falhou (ex: `"update"`)
|
|
40
|
+
- `message` — descrição humana
|
|
41
|
+
- `cause` opcional — erro original (MCP, fs, fetch) pra debug
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Catálogo completo
|
|
46
|
+
|
|
47
|
+
### `ValidationError`
|
|
48
|
+
|
|
49
|
+
**Quando**: `validateConfig(config)` detecta campo ausente, tipo errado, ou
|
|
50
|
+
combinação inválida (ex: `mode: auto` sem `approval_mechanism` quando o target
|
|
51
|
+
requer aprovação).
|
|
52
|
+
|
|
53
|
+
**Campos extras**:
|
|
54
|
+
- `field` — caminho do campo inválido (ex: `"output.targets[0].publishes"`)
|
|
55
|
+
- `expected` — o que era esperado
|
|
56
|
+
- `got` — o que veio
|
|
57
|
+
|
|
58
|
+
**Como o caller reage**: para imediatamente. Nunca é recuperável em runtime —
|
|
59
|
+
o usuário precisa arrumar o `sfw.config.yml` e re-rodar.
|
|
60
|
+
|
|
61
|
+
**Exemplo**:
|
|
62
|
+
```
|
|
63
|
+
ValidationError em confluence.validateConfig:
|
|
64
|
+
field: input.config.space_key
|
|
65
|
+
expected: string não-vazio
|
|
66
|
+
got: undefined
|
|
67
|
+
message: space_key obrigatório pro ConfluenceAdapter
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### `AuthError`
|
|
73
|
+
|
|
74
|
+
**Quando**: backend rejeita credenciais (401, 403, token expirado, MCP retorna
|
|
75
|
+
auth failure). Também lançado se credenciais nem foram encontradas (`.mcp.json`
|
|
76
|
+
ausente, env var não setada).
|
|
77
|
+
|
|
78
|
+
**Campos extras**:
|
|
79
|
+
- `hint` opcional — pista acionável (ex: `"verifique CONFLUENCE_TOKEN em .mcp.json"`)
|
|
80
|
+
|
|
81
|
+
**Como o caller reage**: para imediatamente. Mostra `hint` pro usuário. Nunca
|
|
82
|
+
tenta retry — credenciais não se consertam sozinhas.
|
|
83
|
+
|
|
84
|
+
**Importante**: adapters nunca devem vazar o token no `message` ou `cause`.
|
|
85
|
+
Se o erro original tem o token no URL, o adapter faz sanitize antes de embrulhar.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### `NotFoundError`
|
|
90
|
+
|
|
91
|
+
**Quando**: operação aponta pra um `itemId` / `containerId` que não existe ou
|
|
92
|
+
foi deletado. Inclui: `fetchContent`, `getVersion`, `update`, `listChildren`,
|
|
93
|
+
`fetchAttachments` em um ID inválido.
|
|
94
|
+
|
|
95
|
+
**Campos extras**:
|
|
96
|
+
- `itemId` — o ID que não foi encontrado
|
|
97
|
+
- `kind` — `"container"` | `"item"` | `"attachment"`
|
|
98
|
+
|
|
99
|
+
**Como o caller reage**:
|
|
100
|
+
- `/load`: pode ser scope removido no backend. Loga e pula (não fatal).
|
|
101
|
+
- `/publish`: se título existia no publish-log mas sumiu do backend → o caller
|
|
102
|
+
DEVE chamar `create` em vez de `update` (fallback implícito).
|
|
103
|
+
- `/new-project`: se parent root não existe → fatal (config errada).
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `ConflictError`
|
|
108
|
+
|
|
109
|
+
**Quando**:
|
|
110
|
+
- `create` encontra título duplicado no parent (ou no space, no Confluence).
|
|
111
|
+
- `update` detecta `expectedVersion != version atual` (drift — alguém editou
|
|
112
|
+
entre o último `getVersion` e o `update`).
|
|
113
|
+
|
|
114
|
+
**Campos extras**:
|
|
115
|
+
- `expectedVersion` — o que o caller achava que estava lá
|
|
116
|
+
- `actualVersion` — o que o backend tem de fato
|
|
117
|
+
- `itemId` — item que conflitou
|
|
118
|
+
- `kind` — `"duplicate_title"` | `"version_drift"`
|
|
119
|
+
|
|
120
|
+
**Como o caller reage**:
|
|
121
|
+
- `duplicate_title` (no `create`): normalmente fatal em MVP — skills assumem
|
|
122
|
+
naming unique via template com `{scope}` no título. Se bateu, dois
|
|
123
|
+
scopes têm o mesmo nome — o user precisa renomear no Input.
|
|
124
|
+
- `version_drift` (no `update`): **recuperável**. Fluxo:
|
|
125
|
+
1. `/publish` para a operação atual
|
|
126
|
+
2. Baixa content remoto via `fetchContent`
|
|
127
|
+
3. Gera diff local-vs-remoto
|
|
128
|
+
4. Pergunta ao usuário: **sobrescrever, abortar, ou fazer merge manual**
|
|
129
|
+
5. Se sobrescrever → novo `update` com `expectedVersion` atualizada
|
|
130
|
+
|
|
131
|
+
**Nunca retry automático** — drift é sinal de que um humano tocou o artefato
|
|
132
|
+
(aprovação manual, edição pós-review). Requer decisão consciente.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### `TransportError`
|
|
137
|
+
|
|
138
|
+
**Quando**: rede caiu, MCP morreu, timeout, 5xx do backend.
|
|
139
|
+
|
|
140
|
+
**Campos extras**:
|
|
141
|
+
- `retryable` — `true` se faz sentido tentar de novo (5xx, timeout)
|
|
142
|
+
- `statusCode` opcional — se o backend deu HTTP
|
|
143
|
+
|
|
144
|
+
**Como o caller reage**:
|
|
145
|
+
- `retryable=true`: retry com backoff (proposta: 3 tentativas, 1s / 3s / 10s).
|
|
146
|
+
Depois disso, escala pro usuário.
|
|
147
|
+
- `retryable=false`: escala imediato.
|
|
148
|
+
|
|
149
|
+
**Importante**: distinguir `TransportError` de `AuthError` é crítico — 401 **não**
|
|
150
|
+
é transport, é auth. Adapter precisa classificar antes de embrulhar.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
### `NotImplementedError`
|
|
155
|
+
|
|
156
|
+
**Quando**: skill chama método opcional (`attachFile`) num adapter que não
|
|
157
|
+
implementa.
|
|
158
|
+
|
|
159
|
+
**Como o caller reage**: skill **deve** ter fallback ou skipar feature. Nunca
|
|
160
|
+
é fatal — é esperado em MVP (FilesystemAdapter sem attachments na v0, por ex).
|
|
161
|
+
|
|
162
|
+
Caller típico:
|
|
163
|
+
```
|
|
164
|
+
try {
|
|
165
|
+
await adapter.attachFile(id, name, buf, mime);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
if (e instanceof NotImplementedError) {
|
|
168
|
+
log("adapter ${adapter.name} não suporta attachments, pulando");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Quem lança o quê (matriz)
|
|
178
|
+
|
|
179
|
+
| Método | ValidationError | AuthError | NotFoundError | ConflictError | TransportError | NotImplementedError |
|
|
180
|
+
|--------|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
181
|
+
| `validateConfig` | ✅ | — | — | — | — | — |
|
|
182
|
+
| `listChildren` | — | ✅ | ✅ | — | ✅ | — |
|
|
183
|
+
| `fetchContent` | — | ✅ | ✅ | — | ✅ | — |
|
|
184
|
+
| `fetchAttachments` | — | ✅ | ✅ | — | ✅ | — |
|
|
185
|
+
| `getVersion` | — | ✅ | ✅ | — | ✅ | — |
|
|
186
|
+
| `create` | — | ✅ | ✅¹ | ✅² | ✅ | — |
|
|
187
|
+
| `update` | — | ✅ | ✅ | ✅³ | ✅ | — |
|
|
188
|
+
| `attachFile` | — | ✅ | ✅ | — | ✅ | ✅⁴ |
|
|
189
|
+
|
|
190
|
+
**Notas**:
|
|
191
|
+
- ¹ `NotFoundError` no `create` quando o `parentId` não existe.
|
|
192
|
+
- ² `ConflictError` no `create` = `kind: "duplicate_title"`.
|
|
193
|
+
- ³ `ConflictError` no `update` = `kind: "version_drift"`.
|
|
194
|
+
- ⁴ `NotImplementedError` apenas em adapters que não suportam attachments.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Padrão de implementação (pseudo-código)
|
|
199
|
+
|
|
200
|
+
Todo adapter embrulha erros nativos nesses tipos. Exemplo genérico:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
async update(itemId, content, expectedVersion) {
|
|
204
|
+
try {
|
|
205
|
+
const current = await this.getVersion(itemId);
|
|
206
|
+
if (current !== expectedVersion) {
|
|
207
|
+
throw new ConflictError({
|
|
208
|
+
adapter: this.name,
|
|
209
|
+
operation: "update",
|
|
210
|
+
kind: "version_drift",
|
|
211
|
+
itemId,
|
|
212
|
+
expectedVersion,
|
|
213
|
+
actualVersion: current,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const result = await this.backendUpdate(itemId, content);
|
|
217
|
+
return { version: result.version, ok: true };
|
|
218
|
+
} catch (e) {
|
|
219
|
+
if (e instanceof ConflictError) throw e;
|
|
220
|
+
if (isNotFound(e)) throw new NotFoundError({ adapter: this.name, operation: "update", itemId, kind: "item", cause: e });
|
|
221
|
+
if (isAuth(e)) throw new AuthError( { adapter: this.name, operation: "update", cause: e });
|
|
222
|
+
if (isTransport(e)) throw new TransportError({ adapter: this.name, operation: "update", retryable: e.status >= 500, cause: e });
|
|
223
|
+
throw e; // unknown — propaga cru
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Referências
|
|
231
|
+
|
|
232
|
+
- Interface: `.github/adapters/interface.md`
|
|
233
|
+
- Registry: `.github/adapters/registry.md`
|
|
234
|
+
- Naming: `.github/adapters/naming.md`
|