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.
- package/README.md +252 -167
- package/bin/cli.js +70 -70
- package/lib/init.js +92 -92
- package/lib/update.js +132 -132
- package/package.json +1 -1
- package/templates/.ai/memory/napkin.md +68 -68
- package/templates/.github/CHANGELOG.md +560 -533
- package/templates/.github/adapters/SETUP.md +314 -314
- package/templates/.github/adapters/confluence.md +295 -295
- package/templates/.github/adapters/errors.md +234 -234
- package/templates/.github/adapters/filesystem.md +353 -353
- package/templates/.github/adapters/interface.md +301 -301
- package/templates/.github/adapters/naming.md +241 -241
- package/templates/.github/adapters/registry.md +244 -244
- package/templates/.github/agents/backend-coder.md +215 -215
- package/templates/.github/agents/db-coder.md +165 -165
- package/templates/.github/agents/doc-writer.md +66 -66
- package/templates/.github/agents/frontend-coder.md +222 -222
- package/templates/.github/agents/infra-coder.md +341 -341
- package/templates/.github/agents/reviewer.md +99 -99
- package/templates/.github/agents/security-reviewer.md +153 -153
- package/templates/.github/copilot-instructions.md +272 -272
- package/templates/.github/instructions/docs.instructions.md +147 -145
- package/templates/.github/instructions/sensitive-files.instructions.md +32 -32
- package/templates/.github/rules.md +229 -229
- package/templates/.github/scripts/bootstrap-confluence.js +289 -289
- package/templates/.github/skills/sf-design/SKILL.md +161 -161
- package/templates/.github/skills/sf-dev/SKILL.md +204 -204
- package/templates/.github/skills/sf-discovery/SKILL.md +415 -415
- package/templates/.github/skills/sf-extract/SKILL.md +225 -225
- package/templates/.github/skills/sf-load/SKILL.md +296 -296
- package/templates/.github/skills/sf-mcp/SKILL.md +386 -386
- package/templates/.github/skills/sf-merge-docs/SKILL.md +152 -152
- package/templates/.github/skills/sf-plan/SKILL.md +152 -152
- package/templates/.github/skills/sf-publish/SKILL.md +144 -144
- package/templates/.github/skills/sf-session-finish/SKILL.md +93 -93
- package/templates/.github/skills/sf-start/SKILL.md +192 -192
- package/templates/.github/templates/estrutura/apiContracts.template.md +160 -159
- package/templates/.github/templates/estrutura/architecture.template.md +169 -168
- package/templates/.github/templates/estrutura/conventions.template.md +214 -212
- package/templates/.github/templates/estrutura/decisions.template.md +107 -107
- package/templates/.github/templates/estrutura/domain.template.md +161 -160
- package/templates/.github/templates/feature/PRD.template.md +279 -279
- package/templates/.github/templates/feature/Progresso.template.md +141 -141
- package/templates/.github/templates/feature/TRD.template.md +358 -358
- package/templates/.github/templates/feature/context.template.md +89 -89
- package/templates/.github/templates/feature/extract-log.template.md +49 -49
- package/templates/.github/templates/feature/projetos.template.yaml +79 -79
- package/templates/.github/templates/global/progresso_global.template.md +59 -57
- package/templates/.github/templates/specs/brief.template.md +66 -66
- package/templates/.github/templates/specs/contracts.template.md +147 -147
- package/templates/.github/templates/specs/scenarios.template.md +125 -125
- package/templates/.github/templates/specs/tasks.template.md +65 -65
- package/templates/_gitignore +35 -35
- 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)
|