spec-first-copilot 0.5.0-beta.16 → 0.5.0-beta.18
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/package.json
CHANGED
|
@@ -1,134 +1,130 @@
|
|
|
1
|
-
# /sf-load <nome>
|
|
1
|
+
# /sf-load <nome> | --all
|
|
2
2
|
|
|
3
|
-
Puxa
|
|
4
|
-
e materializa em `workspace/Input/{nome}/` no projeto local.
|
|
3
|
+
Puxa conteúdo do backend configurado (Confluence, filesystem, etc.) e materializa localmente.
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## Modos
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
| Modo | O que traz | Destino local |
|
|
8
|
+
|------|-----------|---------------|
|
|
9
|
+
| `/sf-load <nome>` | Input **e** Output do scope `{nome}` | `workspace/Input/{nome}/` + `workspace/Output/{nome}/` |
|
|
10
|
+
| `/sf-load --all` | **Tudo** — todos os scopes de Input e Output | `workspace/Input/*/` + `workspace/Output/*/` |
|
|
11
|
+
|
|
12
|
+
## Por que trazer Output também?
|
|
13
|
+
|
|
14
|
+
O Output pode já ter artefatos publicados (TRD, SDD, Progresso) por outra sessão
|
|
15
|
+
ou outro dev. Se você não traz, o pipeline pensa que não existe e tenta recriar.
|
|
16
|
+
Trazer Output garante continuidade.
|
|
10
17
|
|
|
11
18
|
## Quando usar
|
|
12
19
|
|
|
13
|
-
- **Antes de `/sf-new-project
|
|
14
|
-
|
|
15
|
-
- **
|
|
16
|
-
|
|
17
|
-
- **Não obrigatório** se o usuário já colocou insumos direto em `workspace/Input/{nome}/`
|
|
20
|
+
- **Antes de `/sf-new-project` ou `/sf-feature`** — traz insumos frescos + artefatos existentes
|
|
21
|
+
- **Após o PM adicionar novos insumos** — re-sincroniza incrementalmente
|
|
22
|
+
- **Início de sessão num projeto existente** — `/sf-load --all` pra ter tudo atualizado
|
|
23
|
+
- **Não obrigatório** se o projeto é 100% local (sem `sfw.config.yml`)
|
|
18
24
|
|
|
19
25
|
## Pré-condições
|
|
20
26
|
|
|
21
27
|
| # | Validação | Se falhar |
|
|
22
28
|
|---|-----------|-----------|
|
|
23
|
-
| 1 | `sfw.config.yml` existe na raiz do projeto | Parar → "
|
|
24
|
-
| 2 | `sfw.config.yml` tem seção `input`
|
|
25
|
-
| 3 | Adapter
|
|
29
|
+
| 1 | `sfw.config.yml` existe na raiz do projeto | Parar → "Rode /sf-mcp confluence ou configure manualmente. Veja .github/adapters/SETUP.md" |
|
|
30
|
+
| 2 | `sfw.config.yml` tem seção `input` e `output` | Parar → "Seções input/output não encontradas no sfw.config.yml" |
|
|
31
|
+
| 3 | Adapter acessível (MCP rodando pra Confluence) | Parar → orientar setup conforme `.github/adapters/SETUP.md` |
|
|
26
32
|
|
|
27
33
|
## Execução
|
|
28
34
|
|
|
29
35
|
### 1. Carregar configuração
|
|
30
36
|
|
|
31
37
|
Ler `sfw.config.yml` e extrair:
|
|
32
|
-
- `input.adapter`
|
|
33
|
-
- `input.config` —
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
- `input.cache.
|
|
37
|
-
- `
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
**Para Filesystem** (`adapter: filesystem`):
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
1. Listar conteúdo de config.root_path
|
|
56
|
-
2. Buscar pasta ou arquivo com nome == {nome}
|
|
57
|
-
3. Se não encontrou → ERRO: "Scope '{nome}' não encontrado em {root_path}"
|
|
58
|
-
4. Anotar scope_path
|
|
59
|
-
```
|
|
38
|
+
- `input.adapter` + `input.config` — pra puxar Input
|
|
39
|
+
- `input.config.parent_page_id` — page-mãe do Input no backend
|
|
40
|
+
- `output.targets[0].adapter` + `output.targets[0].config` — pra puxar Output
|
|
41
|
+
- `output.targets[0].config.parent_page_id` — page-mãe do Output no backend
|
|
42
|
+
- `input.cache.local_dir` — default `workspace/Input/`
|
|
43
|
+
- `input.cache.log` — default `.ai/sf-load-log.md`
|
|
44
|
+
- `naming.output_container` — template pra nome da pasta de Output (ex: `out_{scope}`)
|
|
45
|
+
|
|
46
|
+
### 2. Determinar o que trazer
|
|
47
|
+
|
|
48
|
+
**Se `/sf-load <nome>`:**
|
|
49
|
+
- **Input**: buscar scope `{nome}` nos filhos de `input.parent_page_id`
|
|
50
|
+
- Match exato por título — se não encontrou, ERRO
|
|
51
|
+
- **Output**: buscar container `out_{nome}` (ou conforme `naming.output_container`) nos filhos de `output.parent_page_id`
|
|
52
|
+
- Se não encontrou → OK, ainda não foi publicado. Pular silenciosamente.
|
|
53
|
+
|
|
54
|
+
**Se `/sf-load --all`:**
|
|
55
|
+
- **Input**: listar TODOS os filhos de `input.parent_page_id`
|
|
56
|
+
- **Output**: listar TODOS os filhos de `output.parent_page_id`
|
|
57
|
+
- Processar cada um como se fosse `/sf-load <nome>` individual
|
|
60
58
|
|
|
61
59
|
### 3. Trazer conteúdo — RECURSIVO ATÉ O ÚLTIMO NÍVEL, FLAT LOCAL
|
|
62
60
|
|
|
63
61
|
> **3 REGRAS INVIOLÁVEIS:**
|
|
64
|
-
> 1. **Descer até o último nível** —
|
|
65
|
-
> 2. **Local é flat** — todos os arquivos
|
|
66
|
-
> 3. **Refletir 100% do externo** — tudo que existe no backend
|
|
62
|
+
> 1. **Descer até o último nível** — `get_page_children` retorna só 1 nível. O agent DEVE fazer loop recursivo.
|
|
63
|
+
> 2. **Local é flat** — todos os arquivos soltos na pasta, SEM subpastas por nível.
|
|
64
|
+
> 3. **Refletir 100% do externo** — tudo que existe no backend DEVE existir localmente.
|
|
67
65
|
|
|
68
|
-
**Para
|
|
66
|
+
**Para cada scope de Input — materializar em `workspace/Input/{nome}/`:**
|
|
69
67
|
|
|
70
68
|
```
|
|
71
69
|
function loadScope(pageId, targetDir):
|
|
72
|
-
// PASSO 1: Coletar TODAS as pages da árvore (recursivo)
|
|
73
70
|
allPages = []
|
|
74
71
|
collectAllPages(pageId, allPages)
|
|
75
72
|
|
|
76
|
-
// PASSO 2: Baixar conteúdo de CADA page como arquivo flat
|
|
77
73
|
para cada page em allPages:
|
|
78
|
-
|
|
74
|
+
// IMPORTANTE: SEMPRE pedir markdown, NUNCA storage format (HTML)
|
|
75
|
+
result = mcp__atlassian__confluence_get_page(page_id=page.id, convert_to_markdown=true)
|
|
76
|
+
content = result.content (ou result.body — depende da resposta do MCP)
|
|
77
|
+
|
|
78
|
+
// LIMPEZA OBRIGATÓRIA — Confluence pode vazar HTML mesmo com convert_to_markdown
|
|
79
|
+
// Se o conteúdo contém tags HTML (<p>, <div>, <span>, <table>, etc.):
|
|
80
|
+
// 1. Remover tags de metadata: <p local-id="...">, <ri:...>, <ac:...>
|
|
81
|
+
// 2. Converter tags semânticas pra markdown:
|
|
82
|
+
// <h1> → #, <h2> → ##, <h3> → ###
|
|
83
|
+
// <strong>/<b> → **texto**
|
|
84
|
+
// <em>/<i> → *texto*
|
|
85
|
+
// <a href="url"> → [texto](url)
|
|
86
|
+
// <ul>/<li> → - item
|
|
87
|
+
// <ol>/<li> → 1. item
|
|
88
|
+
// <code> → `código`
|
|
89
|
+
// <pre> → ```bloco```
|
|
90
|
+
// <table>/<tr>/<td> → tabela markdown
|
|
91
|
+
// 3. Remover tags restantes sem equivalente markdown (strip tags, manter texto)
|
|
92
|
+
// 4. Limpar linhas vazias excessivas
|
|
93
|
+
// O arquivo salvo DEVE ser markdown limpo, sem nenhuma tag HTML.
|
|
94
|
+
|
|
79
95
|
filename = sanitize(page.title) + ".md"
|
|
80
|
-
salvar content em {targetDir}/{filename}
|
|
81
|
-
registrar no load-log
|
|
96
|
+
salvar content LIMPO em {targetDir}/{filename}
|
|
97
|
+
registrar no load-log
|
|
82
98
|
|
|
83
|
-
//
|
|
99
|
+
// Attachments de CADA page
|
|
84
100
|
para cada page em allPages:
|
|
85
101
|
attachments = mcp__atlassian__confluence_get_attachments(page_id=page.id)
|
|
86
|
-
para cada att
|
|
102
|
+
para cada att:
|
|
87
103
|
bytes = mcp__atlassian__confluence_download_attachment(page_id=page.id, filename=att.title)
|
|
88
|
-
salvar em {targetDir}/{att.title}
|
|
104
|
+
salvar em {targetDir}/{att.title}
|
|
89
105
|
registrar no load-log
|
|
90
106
|
|
|
91
107
|
function collectAllPages(pageId, result):
|
|
92
|
-
// get_page_children retorna APENAS filhos diretos — por isso o loop recursivo
|
|
93
108
|
children = mcp__atlassian__confluence_get_page_children(page_id=pageId)
|
|
94
|
-
para cada child
|
|
95
|
-
result.push(child)
|
|
96
|
-
collectAllPages(child.id, result)
|
|
109
|
+
para cada child:
|
|
110
|
+
result.push(child)
|
|
111
|
+
collectAllPages(child.id, result) // recursão total
|
|
97
112
|
```
|
|
98
113
|
|
|
99
|
-
**
|
|
100
|
-
```
|
|
101
|
-
app_barbearia (page raiz do scope)
|
|
102
|
-
├── Requisitos funcionais
|
|
103
|
-
│ ├── Regras de negocio
|
|
104
|
-
│ │ └── Neta da pagina ← 3º nível!
|
|
105
|
-
│ └── Jornadas
|
|
106
|
-
├── Stack tecnica
|
|
107
|
-
└── attachment: diagrama.png
|
|
108
|
-
```
|
|
114
|
+
**Para cada scope de Output — materializar em `workspace/Output/{nome}/`:**
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
├── regras_de_negocio.md ← neto
|
|
116
|
-
├── neta_da_pagina.md ← bisneto (3º nível!)
|
|
117
|
-
├── jornadas.md ← filho
|
|
118
|
-
├── stack_tecnica.md ← filho
|
|
119
|
-
└── diagrama.png ← attachment
|
|
120
|
-
```
|
|
116
|
+
Mesmo processo, mas:
|
|
117
|
+
- Container no backend tem nome conforme `naming.output_container` (ex: `out_app_barbearia`)
|
|
118
|
+
- Artifacts dentro dele são TRD, SDD, Progresso, etc.
|
|
119
|
+
- Materializar cada um como `{tipo}.md` (ex: `TRD.md`, `sdd.md`, `Progresso.md`)
|
|
120
|
+
- Nome local do scope é extraído do container (remove prefixo `out_`)
|
|
121
121
|
|
|
122
|
-
**
|
|
123
|
-
|
|
124
|
-
**Para Filesystem**:
|
|
122
|
+
**Para Filesystem** (`adapter: filesystem`):
|
|
125
123
|
|
|
126
124
|
```
|
|
127
125
|
function loadScope(sourcePath, targetDir):
|
|
128
|
-
// Se source == targetDir → no-op
|
|
126
|
+
// Se source == targetDir → no-op
|
|
129
127
|
// Senão: copiar TODOS os arquivos recursivamente pra targetDir (flat)
|
|
130
|
-
// Subpastas do source viram arquivos flat no target
|
|
131
|
-
// Calcular sha256 de cada arquivo pra load-log
|
|
132
128
|
```
|
|
133
129
|
|
|
134
130
|
### 4. Log incremental (`.ai/sf-load-log.md`)
|
|
@@ -138,85 +134,93 @@ Formato append-only:
|
|
|
138
134
|
```markdown
|
|
139
135
|
## Load: {nome} — {ISO_DATETIME}
|
|
140
136
|
|
|
137
|
+
### Input
|
|
141
138
|
| Item ID | Title | Version | SHA256 | Local Path | Status |
|
|
142
139
|
|---------|-------|---------|--------|------------|--------|
|
|
143
140
|
| 294950 | app_barbearia | 3 | a3f5... | workspace/Input/app_barbearia/app_barbearia.md | NOVO |
|
|
144
|
-
| 491572 | Requisitos
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
|
148
|
-
|
|
149
|
-
|
|
|
141
|
+
| 491572 | Requisitos | 2 | 8b1c... | workspace/Input/app_barbearia/requisitos.md | NOVO |
|
|
142
|
+
|
|
143
|
+
### Output
|
|
144
|
+
| Item ID | Title | Version | SHA256 | Local Path | Status |
|
|
145
|
+
|---------|-------|---------|--------|------------|--------|
|
|
146
|
+
| 393238 | app_barbearia - TRD | 2 | 4d2a... | workspace/Output/app_barbearia/TRD.md | NOVO |
|
|
147
|
+
| 425998 | app_barbearia - SDD | 1 | 7f3e... | workspace/Output/app_barbearia/sdd.md | NOVO |
|
|
150
148
|
```
|
|
151
149
|
|
|
152
150
|
**Se `incremental: true`** (default):
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
151
|
+
- Comparar hash com log anterior antes de baixar
|
|
152
|
+
- `INALTERADO` = hash igual, não sobrescreve
|
|
153
|
+
- `MODIFICADO` = hash diferente, sobrescreve
|
|
154
|
+
- `NOVO` = não existia no log
|
|
155
|
+
- `REMOVIDO` = existia no log mas sumiu do backend (log only, não deleta local)
|
|
158
156
|
|
|
159
157
|
### 5. Ajustar `.gitignore` conforme o adapter
|
|
160
158
|
|
|
161
|
-
Se o adapter NÃO é `filesystem
|
|
162
|
-
- Verificar se `.gitignore` contém
|
|
163
|
-
- Se NÃO contém,
|
|
159
|
+
Se o adapter NÃO é `filesystem`:
|
|
160
|
+
- Verificar se `.gitignore` contém `# SFW: workspace ignored`
|
|
161
|
+
- Se NÃO contém, adicionar:
|
|
164
162
|
```
|
|
165
163
|
# SFW: workspace ignored (external backend detected)
|
|
166
|
-
# Content lives in the external backend (Confluence, etc.) — local is just cache
|
|
167
164
|
workspace/Output/**
|
|
168
165
|
!workspace/Output/.gitkeep
|
|
169
166
|
```
|
|
170
|
-
- Informar ao usuário: "workspace/Output/ adicionado ao .gitignore (conteúdo vive no {adapter})"
|
|
171
167
|
|
|
172
|
-
Se o adapter É `filesystem
|
|
173
|
-
- Se `.gitignore` contém o bloco `# SFW: workspace ignored`, **removê-lo**
|
|
174
|
-
- Informar ao usuário: "workspace/Output/ removido do .gitignore (modo local)"
|
|
175
|
-
|
|
176
|
-
Isso garante que:
|
|
177
|
-
- Projetos com Confluence não commitam cache local
|
|
178
|
-
- Projetos que mudam de Confluence → filesystem voltam a rastrear
|
|
179
|
-
- A lógica roda no momento certo (após configurar `sfw.config.yml`, não no `init`)
|
|
168
|
+
Se o adapter É `filesystem` e o bloco existe, removê-lo.
|
|
180
169
|
|
|
181
170
|
### 6. Informar o usuário
|
|
182
171
|
|
|
172
|
+
**Para `/sf-load <nome>`:**
|
|
183
173
|
```
|
|
184
|
-
|
|
174
|
+
Scope "{nome}" carregado:
|
|
185
175
|
|
|
186
|
-
{N} arquivos ({X novos, Y modificados, Z inalterados})
|
|
187
|
-
{M}
|
|
176
|
+
Input: {N} arquivos ({X novos, Y modificados, Z inalterados})
|
|
177
|
+
Output: {M} artefatos ({A novos, B modificados, C inalterados})
|
|
178
|
+
ou "nenhum artefato publicado ainda"
|
|
188
179
|
|
|
189
180
|
Próximo passo:
|
|
190
|
-
/sf-new-project {nome} ←
|
|
191
|
-
/sf-feature {nome} ←
|
|
181
|
+
/sf-new-project {nome} ← bootstrap técnico (gera TRD)
|
|
182
|
+
/sf-feature {nome} ← feature (gera PRD)
|
|
183
|
+
|
|
184
|
+
Log: .ai/sf-load-log.md
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Para `/sf-load --all`:**
|
|
188
|
+
```
|
|
189
|
+
Projeto sincronizado:
|
|
190
|
+
|
|
191
|
+
Input: {N} scopes, {X} arquivos total
|
|
192
|
+
Output: {M} scopes, {Y} artefatos total
|
|
193
|
+
|
|
194
|
+
Scopes encontrados:
|
|
195
|
+
- app_barbearia (Input: 5 arquivos, Output: TRD + SDD + Progresso)
|
|
196
|
+
- feat_login (Input: 3 arquivos, Output: nenhum)
|
|
197
|
+
- feat_pagamento (Input: 2 arquivos, Output: PRD)
|
|
192
198
|
|
|
193
|
-
Log
|
|
199
|
+
Log: .ai/sf-load-log.md
|
|
194
200
|
```
|
|
195
201
|
|
|
196
202
|
## Notas
|
|
197
203
|
|
|
198
|
-
- `/sf-load` é **idempotente** — rodar N vezes
|
|
199
|
-
|
|
200
|
-
-
|
|
201
|
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
- **Estrutura local é FLAT** — não replica hierarquia do backend. Todos os arquivos soltos na mesma pasta
|
|
208
|
-
- **Recursão é total** — desce até o último nível. Se `get_page_children` retornou filhos, DESCER em cada um
|
|
204
|
+
- `/sf-load` é **idempotente** — rodar N vezes não muda nada se backend não mudou
|
|
205
|
+
- `/sf-load` **nunca modifica o backend** — só lê
|
|
206
|
+
- `/sf-load` **nunca deleta arquivos locais** — marca REMOVIDO no log mas mantém
|
|
207
|
+
- Attachments ficam flat junto com os .md
|
|
208
|
+
- **Estrutura local é FLAT** — não replica hierarquia do backend
|
|
209
|
+
- **Recursão é total** — desce até o último nível
|
|
210
|
+
- Nomes sanitizados: espaços → `_`, caracteres especiais removidos
|
|
211
|
+
- Output que não existe no backend é pulado silenciosamente (ainda não publicado)
|
|
212
|
+
- `/sf-load --all` é útil no início de sessão pra ter tudo atualizado
|
|
209
213
|
|
|
210
214
|
## Erros
|
|
211
215
|
|
|
212
216
|
| Erro | Ação |
|
|
213
217
|
|------|------|
|
|
214
|
-
| `sfw.config.yml` não existe | Parar
|
|
215
|
-
|
|
|
216
|
-
|
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
|
|
|
218
|
+
| `sfw.config.yml` não existe | Parar → orientar /sf-mcp ou setup manual |
|
|
219
|
+
| MCP não disponível | Parar → "Reinicie o Claude Code" |
|
|
220
|
+
| Scope não encontrado no Input | Parar, listar scopes disponíveis |
|
|
221
|
+
| Auth error (401/403) | Parar → "Verifique .mcp.json" |
|
|
222
|
+
| Page sem conteúdo | OK — salvar vazio, registrar no log |
|
|
223
|
+
| Output não tem container pro scope | OK — pular, informar "nenhum artefato publicado ainda" |
|
|
220
224
|
|
|
221
225
|
## Referências
|
|
222
226
|
|