spec-first-copilot 0.7.0-beta.1 → 0.7.0

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 (55) hide show
  1. package/README.md +252 -167
  2. package/bin/cli.js +70 -70
  3. package/lib/init.js +92 -92
  4. package/lib/update.js +132 -132
  5. package/package.json +1 -1
  6. package/templates/.ai/memory/napkin.md +68 -68
  7. package/templates/.github/CHANGELOG.md +560 -533
  8. package/templates/.github/adapters/SETUP.md +314 -314
  9. package/templates/.github/adapters/confluence.md +295 -295
  10. package/templates/.github/adapters/errors.md +234 -234
  11. package/templates/.github/adapters/filesystem.md +353 -353
  12. package/templates/.github/adapters/interface.md +301 -301
  13. package/templates/.github/adapters/naming.md +241 -241
  14. package/templates/.github/adapters/registry.md +244 -244
  15. package/templates/.github/agents/backend-coder.md +215 -215
  16. package/templates/.github/agents/db-coder.md +165 -165
  17. package/templates/.github/agents/doc-writer.md +66 -66
  18. package/templates/.github/agents/frontend-coder.md +222 -222
  19. package/templates/.github/agents/infra-coder.md +341 -341
  20. package/templates/.github/agents/reviewer.md +99 -99
  21. package/templates/.github/agents/security-reviewer.md +153 -153
  22. package/templates/.github/copilot-instructions.md +272 -272
  23. package/templates/.github/instructions/docs.instructions.md +147 -145
  24. package/templates/.github/instructions/sensitive-files.instructions.md +32 -32
  25. package/templates/.github/rules.md +229 -229
  26. package/templates/.github/scripts/bootstrap-confluence.js +289 -289
  27. package/templates/.github/skills/sf-design/SKILL.md +161 -161
  28. package/templates/.github/skills/sf-dev/SKILL.md +204 -204
  29. package/templates/.github/skills/sf-discovery/SKILL.md +415 -415
  30. package/templates/.github/skills/sf-extract/SKILL.md +225 -225
  31. package/templates/.github/skills/sf-load/SKILL.md +296 -296
  32. package/templates/.github/skills/sf-mcp/SKILL.md +386 -386
  33. package/templates/.github/skills/sf-merge-docs/SKILL.md +152 -152
  34. package/templates/.github/skills/sf-plan/SKILL.md +152 -152
  35. package/templates/.github/skills/sf-publish/SKILL.md +144 -144
  36. package/templates/.github/skills/sf-session-finish/SKILL.md +93 -93
  37. package/templates/.github/skills/sf-start/SKILL.md +192 -192
  38. package/templates/.github/templates/estrutura/apiContracts.template.md +160 -159
  39. package/templates/.github/templates/estrutura/architecture.template.md +169 -168
  40. package/templates/.github/templates/estrutura/conventions.template.md +214 -212
  41. package/templates/.github/templates/estrutura/decisions.template.md +107 -107
  42. package/templates/.github/templates/estrutura/domain.template.md +161 -160
  43. package/templates/.github/templates/feature/PRD.template.md +279 -279
  44. package/templates/.github/templates/feature/Progresso.template.md +141 -141
  45. package/templates/.github/templates/feature/TRD.template.md +358 -358
  46. package/templates/.github/templates/feature/context.template.md +89 -89
  47. package/templates/.github/templates/feature/extract-log.template.md +49 -49
  48. package/templates/.github/templates/feature/projetos.template.yaml +79 -79
  49. package/templates/.github/templates/global/progresso_global.template.md +59 -57
  50. package/templates/.github/templates/specs/brief.template.md +66 -66
  51. package/templates/.github/templates/specs/contracts.template.md +147 -147
  52. package/templates/.github/templates/specs/scenarios.template.md +125 -125
  53. package/templates/.github/templates/specs/tasks.template.md +65 -65
  54. package/templates/_gitignore +35 -35
  55. package/templates/sfw.config.yml.example +147 -147
@@ -1,295 +1,295 @@
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
- > **ATENÇÃO**: `get_page_children` retorna apenas filhos **diretos** — NÃO é recursivo.
58
- > Para trazer a árvore inteira, o caller (ex: `/sf-load`) deve implementar recursão manual.
59
-
60
- **Passos**:
61
- 1. Chamar tool com `page_id: containerId`
62
- 2. Para cada filho retornado, montar `Item`:
63
- - `id` = `page.id` (string)
64
- - `title` = `page.title`
65
- - `version` = `String(page.version.number)` (se disponível, senão chamar `get_page` pra obter)
66
- - `type` = `"page"` (Confluence só tem páginas — folders são páginas vazias convencionadas)
67
- - `hasChildren` = verificar via `get_page_children` no filho ou assumir `true` por default
68
- 3. Se retorno paginado, iterar até esgotar
69
- 4. Retornar array (apenas filhos diretos — 1 nível)
70
-
71
- **Recursão (obrigatória pro `/sf-load` com `recursive: true`)**:
72
-
73
- O `/sf-load` precisa de toda a árvore. Padrão:
74
-
75
- ```
76
- function listChildrenRecursive(containerId):
77
- filhos = listChildren(containerId) // 1 nível
78
- resultado = [...filhos]
79
- para cada filho em filhos:
80
- se filho.type == "page": // tudo no Confluence é page
81
- netos = listChildrenRecursive(filho.id)
82
- resultado = [...resultado, ...netos]
83
- retornar resultado
84
- ```
85
-
86
- Custo: 1 chamada MCP por nó da árvore. Uma árvore com 5 scopes × 3 filhos = ~20 chamadas.
87
- É aceitável — MCP é local (uvx), não HTTP. Se ficar lento com árvores grandes,
88
- considerar cache em `.ai/load-log.md` e re-scan incremental.
89
-
90
- **Erros**:
91
- - `404` → `NotFoundError { kind: "container", itemId: containerId }`
92
- - `401/403` → `AuthError`
93
- - `5xx`/timeout → `TransportError { retryable: true }`
94
-
95
- ---
96
-
97
- ### `fetchContent(itemId) → { content, metadata }`
98
-
99
- **MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: true`
100
-
101
- **Passos**:
102
- 1. Chamar tool com `page_id: itemId`, `convert_to_markdown: true`, `include_metadata: true`
103
- 2. Extrair `content` do campo markdown retornado
104
- 3. Montar `metadata`:
105
- - `version` = `page.version.number` (inteiro)
106
- - `space_key`, `parent_id`, `created_at`, `updated_at`, `author`
107
- - `labels` = array das labels da página (usado pelo caller pra checar `sfw-approved`)
108
- 4. Retornar `{ content, metadata }`
109
-
110
- **Notas importantes**:
111
- - Markdown roundtrip **normaliza** (`#` vira `===` no H1, `-` vira `*` em listas).
112
- `version.number` é a fonte de verdade pra drift, **não** diff de bytes.
113
- - `labels` vêm via `confluence_get_labels` **separadamente** se não vierem
114
- embutidos na resposta do `get_page`. Chamar só se o caller pediu (lazy).
115
-
116
- **Erros**: mesmas regras do `listChildren`.
117
-
118
- ---
119
-
120
- ### `fetchAttachments(itemId) → Attachment[]`
121
-
122
- **MCP**:
123
- - `mcp__atlassian__confluence_get_attachments` (lista)
124
- - `mcp__atlassian__confluence_download_attachment` (bytes, lazy)
125
-
126
- **Passos**:
127
- 1. Se `config.include_attachments === false`, retornar `[]` sem chamar MCP.
128
- 2. Chamar `get_attachments` com `page_id: itemId`
129
- 3. Para cada item retornado, montar `Attachment`:
130
- - `filename` = `attachment.title`
131
- - `size` = `attachment.size` em bytes
132
- - `mimeType` = `attachment.mime_type`
133
- - `download()` = closure que chama `confluence_download_attachment` com
134
- `page_id: itemId, filename: attachment.title` e retorna `Buffer`
135
- 4. Retornar array
136
-
137
- **Lazy**: `download()` só dispara quando o caller chama. Listar attachments é
138
- barato, baixar PDFs pode não ser.
139
-
140
- ---
141
-
142
- ### `getVersion(itemId) → string`
143
-
144
- **MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: false`
145
-
146
- **Passos**:
147
- 1. Chamar tool com `page_id: itemId`, `include_metadata: true`, `convert_to_markdown: false`
148
- (evita o custo de converter body quando só queremos version)
149
- 2. Retornar `String(page.version.number)`
150
-
151
- **Nota**: se o MCP oferecer um `confluence_get_page_history` mais barato no
152
- futuro, trocar. Hoje `get_page` é o caminho mais direto. Custo: ~1 request HTTP.
153
-
154
- ---
155
-
156
- ### `create(parentId, title, content) → { itemId, version }`
157
-
158
- **MCP**: `mcp__atlassian__confluence_create_page`
159
-
160
- **Passos**:
161
- 1. Chamar tool com:
162
- - `space_key: config.space_key`
163
- - `parent_id: parentId`
164
- - `title: title` (já vem formatado pelo naming engine — ex: `"app_barbearia - PRD"`)
165
- - `content: content` (markdown — MCP converte pra storage format)
166
- - `content_format: "markdown"`
167
- 2. Retornar:
168
- - `itemId` = `response.page.id`
169
- - `version` = `String(response.page.version.number)` (tipicamente `"1"`)
170
-
171
- **Erros específicos**:
172
- - `BadRequestException: A page already exists with the same TITLE in this space` →
173
- `ConflictError { kind: "duplicate_title", itemId: null }`
174
- (naming templating com `{scope}` no título deveria impedir isso — se bateu,
175
- dois scopes têm o mesmo nome, o user precisa renomear no Input)
176
- - `parent_id` inválido → `NotFoundError { kind: "container", itemId: parentId }`
177
-
178
- ---
179
-
180
- ### `update(itemId, content, expectedVersion) → { version, ok, conflict? }`
181
-
182
- **MCP**: `mcp__atlassian__confluence_update_page` (+ `get_page` pra version check)
183
-
184
- > **Atenção crítica**: o MCP sooperset **não** aceita `expectedVersion` como
185
- > parâmetro. Isso significa **zero optimistic locking server-side**. Toda a
186
- > conflict detection é client-side — responsabilidade deste adapter.
187
-
188
- **Passos**:
189
- 1. Chamar `getVersion(itemId)` → `currentVersion`
190
- - Se o item sumiu → `NotFoundError`
191
- 2. Comparar `currentVersion !== expectedVersion`:
192
- - **Se diverge** → retornar `{ version: currentVersion, ok: false, conflict: true }`
193
- (sem chamar `update_page` — não sobrescreve drift)
194
- 3. Se bate, chamar `confluence_update_page` com:
195
- - `page_id: itemId`
196
- - `title: <mesmo título atual>` (pegar do `get_page` anterior se não vier em cache)
197
- - `content: content`
198
- - `content_format: "markdown"`
199
- - `version_comment: "sfw: auto-publish"` (opcional mas recomendado pra histórico Confluence)
200
- 4. Retornar `{ version: String(response.page.version.number), ok: true }`
201
-
202
- **Race window**: há uma pequena janela entre o `getVersion` e o `update_page`
203
- onde outra pessoa pode ter editado. MVP aceita — o caso é raro e o próximo
204
- `getVersion`/`update` no ciclo detecta. Se virar dor, documentar como
205
- limitação conhecida.
206
-
207
- **Erros específicos**:
208
- - `version_drift` detectado no passo 2 → retorno `{ ok: false, conflict: true }`
209
- (não lança erro — é caminho normal do contrato)
210
- - Qualquer outro erro → embrulhar conforme `errors.md`
211
-
212
- ---
213
-
214
- ### `attachFile?(itemId, filename, content, mimeType)`
215
-
216
- **MCP**: `mcp__atlassian__confluence_upload_attachment` (ou `upload_attachments` batch)
217
-
218
- **Passos**:
219
- 1. Chamar tool com `page_id: itemId`, `filename`, `content` (bytes), `mime_type`
220
- 2. Não retorna valor — sucesso é ausência de erro
221
-
222
- **Implementação opcional no MVP**: ativar se/quando o `/sf-publish` precisar
223
- anexar diagramas, planilhas, etc. Por padrão, não chamado.
224
-
225
- ---
226
-
227
- ## Constraints descobertos (aprendizado do teste E2E)
228
-
229
- 1. **Title uniqueness por SPACE inteiro** — não por parent.
230
- **Premissa**: 1 space = 1 projeto. Multi-projeto no mesmo space causa
231
- colisão se dois scopes tiverem o mesmo nome.
232
- **Mitigação**: user nomeia items no Input com nomes únicos; naming
233
- templating com `{scope}` no `output_artifact` garante unicidade no Output.
234
-
235
- 2. **Markdown roundtrip é lossy** — `#` vira `===`, `-` vira `*`, emojis
236
- podem normalizar. **Consequência**: nunca usar diff de bytes pra detectar
237
- drift. `version.number` é a única fonte de verdade.
238
-
239
- 3. **`.mcp.json` com `${VAR}` não funciona** no startup do Claude Code.
240
- Credenciais precisam estar hardcoded no arquivo (gitignored). Mudanças
241
- exigem reiniciar Claude Code — servers MCP só sobem no startup.
242
-
243
- 4. **`page_id` é chave estável** — renomeação preserva ID, history, children.
244
- Skills persistem ID em logs, não título.
245
-
246
- 5. **`recursive: true` no `get_page_children`** não é suportado pelo MCP
247
- atual — adapter precisa fazer recursão manual (loop + stack) quando
248
- `config.recursive === true`.
249
-
250
- 6. **Paginação**: `get_page_children` retorna `limit` default 25. Caller
251
- deve iterar via `start` ou cursor pra esgotar filhos — não assumir
252
- que primeira página é tudo.
253
-
254
- ---
255
-
256
- ## Checklist de pré-requisitos (documentar no bootstrap do kit)
257
-
258
- Antes de usar este adapter, usuário precisa:
259
-
260
- - [ ] `uvx` instalado (ou `pipx`/`pip`)
261
- - [ ] `.mcp.json` na raiz do projeto com entrada pro `sooperset/mcp-atlassian`
262
- + credenciais hardcoded (NUNCA commitar — está no `.gitignore` do kit)
263
- - [ ] Token Atlassian com scope de leitura + escrita no space alvo
264
- - [ ] `space_key` descoberto (ex: via `confluence_search` ou URL do Confluence)
265
- - [ ] Página raiz do projeto criada (manualmente ou via `/sf-start`)
266
- - [ ] Claude Code reiniciado após qualquer mudança no `.mcp.json`
267
-
268
- Bootstrap do kit (§9.9.P0.3) automatiza isso.
269
-
270
- ---
271
-
272
- ## Matriz de erros Confluence → tipos do SFW
273
-
274
- | Situação Confluence | Tipo lançado | Detalhes |
275
- |---|---|---|
276
- | 401 / token inválido | `AuthError` | `hint`: "verifique token em .mcp.json" |
277
- | 403 / sem permissão | `AuthError` | `hint`: "usuário sem acesso ao space {space_key}" |
278
- | 404 page_id | `NotFoundError` | `kind: "item"` |
279
- | 404 parent_id | `NotFoundError` | `kind: "container"` |
280
- | `BadRequestException` title duplicado | `ConflictError` | `kind: "duplicate_title"` |
281
- | 5xx Confluence down | `TransportError` | `retryable: true` |
282
- | MCP server não responde | `TransportError` | `retryable: true`, `hint`: "reinicie Claude Code" |
283
- | Timeout | `TransportError` | `retryable: true` |
284
- | Attachment muito grande | `TransportError` | `retryable: false`, `hint`: tamanho máx Confluence |
285
-
286
- ---
287
-
288
- ## Referências
289
-
290
- - Interface: `.github/adapters/interface.md`
291
- - Erros: `.github/adapters/errors.md`
292
- - Naming: `.github/adapters/naming.md`
293
- - Registry: `.github/adapters/registry.md`
294
- - Validação E2E: `planodetarefas.md §9.9` + napkin "Estado do Confluence MCP"
295
- - MCP provider: [sooperset/mcp-atlassian](https://github.com/sooperset/mcp-atlassian)
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
+ > **ATENÇÃO**: `get_page_children` retorna apenas filhos **diretos** — NÃO é recursivo.
58
+ > Para trazer a árvore inteira, o caller (ex: `/sf-load`) deve implementar recursão manual.
59
+
60
+ **Passos**:
61
+ 1. Chamar tool com `page_id: containerId`
62
+ 2. Para cada filho retornado, montar `Item`:
63
+ - `id` = `page.id` (string)
64
+ - `title` = `page.title`
65
+ - `version` = `String(page.version.number)` (se disponível, senão chamar `get_page` pra obter)
66
+ - `type` = `"page"` (Confluence só tem páginas — folders são páginas vazias convencionadas)
67
+ - `hasChildren` = verificar via `get_page_children` no filho ou assumir `true` por default
68
+ 3. Se retorno paginado, iterar até esgotar
69
+ 4. Retornar array (apenas filhos diretos — 1 nível)
70
+
71
+ **Recursão (obrigatória pro `/sf-load` com `recursive: true`)**:
72
+
73
+ O `/sf-load` precisa de toda a árvore. Padrão:
74
+
75
+ ```
76
+ function listChildrenRecursive(containerId):
77
+ filhos = listChildren(containerId) // 1 nível
78
+ resultado = [...filhos]
79
+ para cada filho em filhos:
80
+ se filho.type == "page": // tudo no Confluence é page
81
+ netos = listChildrenRecursive(filho.id)
82
+ resultado = [...resultado, ...netos]
83
+ retornar resultado
84
+ ```
85
+
86
+ Custo: 1 chamada MCP por nó da árvore. Uma árvore com 5 scopes × 3 filhos = ~20 chamadas.
87
+ É aceitável — MCP é local (uvx), não HTTP. Se ficar lento com árvores grandes,
88
+ considerar cache em `.ai/load-log.md` e re-scan incremental.
89
+
90
+ **Erros**:
91
+ - `404` → `NotFoundError { kind: "container", itemId: containerId }`
92
+ - `401/403` → `AuthError`
93
+ - `5xx`/timeout → `TransportError { retryable: true }`
94
+
95
+ ---
96
+
97
+ ### `fetchContent(itemId) → { content, metadata }`
98
+
99
+ **MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: true`
100
+
101
+ **Passos**:
102
+ 1. Chamar tool com `page_id: itemId`, `convert_to_markdown: true`, `include_metadata: true`
103
+ 2. Extrair `content` do campo markdown retornado
104
+ 3. Montar `metadata`:
105
+ - `version` = `page.version.number` (inteiro)
106
+ - `space_key`, `parent_id`, `created_at`, `updated_at`, `author`
107
+ - `labels` = array das labels da página (usado pelo caller pra checar `sfw-approved`)
108
+ 4. Retornar `{ content, metadata }`
109
+
110
+ **Notas importantes**:
111
+ - Markdown roundtrip **normaliza** (`#` vira `===` no H1, `-` vira `*` em listas).
112
+ `version.number` é a fonte de verdade pra drift, **não** diff de bytes.
113
+ - `labels` vêm via `confluence_get_labels` **separadamente** se não vierem
114
+ embutidos na resposta do `get_page`. Chamar só se o caller pediu (lazy).
115
+
116
+ **Erros**: mesmas regras do `listChildren`.
117
+
118
+ ---
119
+
120
+ ### `fetchAttachments(itemId) → Attachment[]`
121
+
122
+ **MCP**:
123
+ - `mcp__atlassian__confluence_get_attachments` (lista)
124
+ - `mcp__atlassian__confluence_download_attachment` (bytes, lazy)
125
+
126
+ **Passos**:
127
+ 1. Se `config.include_attachments === false`, retornar `[]` sem chamar MCP.
128
+ 2. Chamar `get_attachments` com `page_id: itemId`
129
+ 3. Para cada item retornado, montar `Attachment`:
130
+ - `filename` = `attachment.title`
131
+ - `size` = `attachment.size` em bytes
132
+ - `mimeType` = `attachment.mime_type`
133
+ - `download()` = closure que chama `confluence_download_attachment` com
134
+ `page_id: itemId, filename: attachment.title` e retorna `Buffer`
135
+ 4. Retornar array
136
+
137
+ **Lazy**: `download()` só dispara quando o caller chama. Listar attachments é
138
+ barato, baixar PDFs pode não ser.
139
+
140
+ ---
141
+
142
+ ### `getVersion(itemId) → string`
143
+
144
+ **MCP**: `mcp__atlassian__confluence_get_page` com `convert_to_markdown: false`
145
+
146
+ **Passos**:
147
+ 1. Chamar tool com `page_id: itemId`, `include_metadata: true`, `convert_to_markdown: false`
148
+ (evita o custo de converter body quando só queremos version)
149
+ 2. Retornar `String(page.version.number)`
150
+
151
+ **Nota**: se o MCP oferecer um `confluence_get_page_history` mais barato no
152
+ futuro, trocar. Hoje `get_page` é o caminho mais direto. Custo: ~1 request HTTP.
153
+
154
+ ---
155
+
156
+ ### `create(parentId, title, content) → { itemId, version }`
157
+
158
+ **MCP**: `mcp__atlassian__confluence_create_page`
159
+
160
+ **Passos**:
161
+ 1. Chamar tool com:
162
+ - `space_key: config.space_key`
163
+ - `parent_id: parentId`
164
+ - `title: title` (já vem formatado pelo naming engine — ex: `"app_barbearia - PRD"`)
165
+ - `content: content` (markdown — MCP converte pra storage format)
166
+ - `content_format: "markdown"`
167
+ 2. Retornar:
168
+ - `itemId` = `response.page.id`
169
+ - `version` = `String(response.page.version.number)` (tipicamente `"1"`)
170
+
171
+ **Erros específicos**:
172
+ - `BadRequestException: A page already exists with the same TITLE in this space` →
173
+ `ConflictError { kind: "duplicate_title", itemId: null }`
174
+ (naming templating com `{scope}` no título deveria impedir isso — se bateu,
175
+ dois scopes têm o mesmo nome, o user precisa renomear no Input)
176
+ - `parent_id` inválido → `NotFoundError { kind: "container", itemId: parentId }`
177
+
178
+ ---
179
+
180
+ ### `update(itemId, content, expectedVersion) → { version, ok, conflict? }`
181
+
182
+ **MCP**: `mcp__atlassian__confluence_update_page` (+ `get_page` pra version check)
183
+
184
+ > **Atenção crítica**: o MCP sooperset **não** aceita `expectedVersion` como
185
+ > parâmetro. Isso significa **zero optimistic locking server-side**. Toda a
186
+ > conflict detection é client-side — responsabilidade deste adapter.
187
+
188
+ **Passos**:
189
+ 1. Chamar `getVersion(itemId)` → `currentVersion`
190
+ - Se o item sumiu → `NotFoundError`
191
+ 2. Comparar `currentVersion !== expectedVersion`:
192
+ - **Se diverge** → retornar `{ version: currentVersion, ok: false, conflict: true }`
193
+ (sem chamar `update_page` — não sobrescreve drift)
194
+ 3. Se bate, chamar `confluence_update_page` com:
195
+ - `page_id: itemId`
196
+ - `title: <mesmo título atual>` (pegar do `get_page` anterior se não vier em cache)
197
+ - `content: content`
198
+ - `content_format: "markdown"`
199
+ - `version_comment: "sfw: auto-publish"` (opcional mas recomendado pra histórico Confluence)
200
+ 4. Retornar `{ version: String(response.page.version.number), ok: true }`
201
+
202
+ **Race window**: há uma pequena janela entre o `getVersion` e o `update_page`
203
+ onde outra pessoa pode ter editado. MVP aceita — o caso é raro e o próximo
204
+ `getVersion`/`update` no ciclo detecta. Se virar dor, documentar como
205
+ limitação conhecida.
206
+
207
+ **Erros específicos**:
208
+ - `version_drift` detectado no passo 2 → retorno `{ ok: false, conflict: true }`
209
+ (não lança erro — é caminho normal do contrato)
210
+ - Qualquer outro erro → embrulhar conforme `errors.md`
211
+
212
+ ---
213
+
214
+ ### `attachFile?(itemId, filename, content, mimeType)`
215
+
216
+ **MCP**: `mcp__atlassian__confluence_upload_attachment` (ou `upload_attachments` batch)
217
+
218
+ **Passos**:
219
+ 1. Chamar tool com `page_id: itemId`, `filename`, `content` (bytes), `mime_type`
220
+ 2. Não retorna valor — sucesso é ausência de erro
221
+
222
+ **Implementação opcional no MVP**: ativar se/quando o `/sf-publish` precisar
223
+ anexar diagramas, planilhas, etc. Por padrão, não chamado.
224
+
225
+ ---
226
+
227
+ ## Constraints descobertos (aprendizado do teste E2E)
228
+
229
+ 1. **Title uniqueness por SPACE inteiro** — não por parent.
230
+ **Premissa**: 1 space = 1 projeto. Multi-projeto no mesmo space causa
231
+ colisão se dois scopes tiverem o mesmo nome.
232
+ **Mitigação**: user nomeia items no Input com nomes únicos; naming
233
+ templating com `{scope}` no `output_artifact` garante unicidade no Output.
234
+
235
+ 2. **Markdown roundtrip é lossy** — `#` vira `===`, `-` vira `*`, emojis
236
+ podem normalizar. **Consequência**: nunca usar diff de bytes pra detectar
237
+ drift. `version.number` é a única fonte de verdade.
238
+
239
+ 3. **`.mcp.json` com `${VAR}` não funciona** no startup do Claude Code.
240
+ Credenciais precisam estar hardcoded no arquivo (gitignored). Mudanças
241
+ exigem reiniciar Claude Code — servers MCP só sobem no startup.
242
+
243
+ 4. **`page_id` é chave estável** — renomeação preserva ID, history, children.
244
+ Skills persistem ID em logs, não título.
245
+
246
+ 5. **`recursive: true` no `get_page_children`** não é suportado pelo MCP
247
+ atual — adapter precisa fazer recursão manual (loop + stack) quando
248
+ `config.recursive === true`.
249
+
250
+ 6. **Paginação**: `get_page_children` retorna `limit` default 25. Caller
251
+ deve iterar via `start` ou cursor pra esgotar filhos — não assumir
252
+ que primeira página é tudo.
253
+
254
+ ---
255
+
256
+ ## Checklist de pré-requisitos (documentar no bootstrap do kit)
257
+
258
+ Antes de usar este adapter, usuário precisa:
259
+
260
+ - [ ] `uvx` instalado (ou `pipx`/`pip`)
261
+ - [ ] `.mcp.json` na raiz do projeto com entrada pro `sooperset/mcp-atlassian`
262
+ + credenciais hardcoded (NUNCA commitar — está no `.gitignore` do kit)
263
+ - [ ] Token Atlassian com scope de leitura + escrita no space alvo
264
+ - [ ] `space_key` descoberto (ex: via `confluence_search` ou URL do Confluence)
265
+ - [ ] Página raiz do projeto criada (manualmente ou via `/sf-start`)
266
+ - [ ] Claude Code reiniciado após qualquer mudança no `.mcp.json`
267
+
268
+ Bootstrap do kit (§9.9.P0.3) automatiza isso.
269
+
270
+ ---
271
+
272
+ ## Matriz de erros Confluence → tipos do SFW
273
+
274
+ | Situação Confluence | Tipo lançado | Detalhes |
275
+ |---|---|---|
276
+ | 401 / token inválido | `AuthError` | `hint`: "verifique token em .mcp.json" |
277
+ | 403 / sem permissão | `AuthError` | `hint`: "usuário sem acesso ao space {space_key}" |
278
+ | 404 page_id | `NotFoundError` | `kind: "item"` |
279
+ | 404 parent_id | `NotFoundError` | `kind: "container"` |
280
+ | `BadRequestException` title duplicado | `ConflictError` | `kind: "duplicate_title"` |
281
+ | 5xx Confluence down | `TransportError` | `retryable: true` |
282
+ | MCP server não responde | `TransportError` | `retryable: true`, `hint`: "reinicie Claude Code" |
283
+ | Timeout | `TransportError` | `retryable: true` |
284
+ | Attachment muito grande | `TransportError` | `retryable: false`, `hint`: tamanho máx Confluence |
285
+
286
+ ---
287
+
288
+ ## Referências
289
+
290
+ - Interface: `.github/adapters/interface.md`
291
+ - Erros: `.github/adapters/errors.md`
292
+ - Naming: `.github/adapters/naming.md`
293
+ - Registry: `.github/adapters/registry.md`
294
+ - Validação E2E: `planodetarefas.md §9.9` + napkin "Estado do Confluence MCP"
295
+ - MCP provider: [sooperset/mcp-atlassian](https://github.com/sooperset/mcp-atlassian)