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,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-first-copilot",
3
- "version": "0.5.0-beta.16",
3
+ "version": "0.5.0-beta.18",
4
4
  "description": "Spec-first workflow kit for GitHub Copilot — AI-driven development with specs, not guesswork",
5
5
  "bin": {
6
6
  "spec-first-copilot": "bin/cli.js"
@@ -1,134 +1,130 @@
1
- # /sf-load <nome>
1
+ # /sf-load <nome> | --all
2
2
 
3
- Puxa os insumos de um scope do backend configurado (Confluence, filesystem, etc.)
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
- ## Argumento
5
+ ## Modos
7
6
 
8
- `<nome>` = nome do item no backend de input (título da page no Confluence,
9
- nome da pasta no filesystem). Match exato — se não encontrar, para com erro.
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 <nome>` ou `/sf-feature <nome>`** para trazer
14
- insumos que o PM publicou no Confluence (ou outro backend)
15
- - **Após o PM adicionar novos insumos** (ex: `ajustes_v1` como page filha)
16
- para re-sincronizar. O log incremental detecta NOVO/MODIFICADO/INALTERADO
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 → "Copie sfw.config.yml.example para sfw.config.yml e configure. Veja .github/adapters/SETUP.md" |
24
- | 2 | `sfw.config.yml` tem seção `input` com `adapter` e `config` | Parar → "Seção input não encontrada no sfw.config.yml" |
25
- | 3 | Adapter do input está acessível (MCP rodando pra Confluence, pasta existe pra filesystem) | Parar → orientar setup conforme `.github/adapters/SETUP.md` |
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` tipo do adapter (`confluence` ou `filesystem`)
33
- - `input.config` — config específica do adapter
34
- - `input.cache.local_dir` — onde materializar (default: `workspace/Input/`)
35
- - `input.cache.log` — arquivo de log (default: `.ai/sf-load-log.md`)
36
- - `input.cache.incremental` — se compara hashes antes de sobrescrever
37
- - `naming.output_container` — template pra nomear a pasta local (usa `{scope}`)
38
-
39
- ### 2. Buscar o scope no backend
40
-
41
- **Para Confluence** (`adapter: confluence`):
42
-
43
- ```
44
- 1. Chamar mcp__atlassian__confluence_get_page_children
45
- com page_id = config.parent_page_id
46
- 2. Na lista de filhos, buscar o que tem title == {nome} (match exato)
47
- 3. Se não encontrou → ERRO: "Scope '{nome}' não encontrado no Confluence
48
- (parent_page_id: {id}). Verifique se a page existe como filha de Input."
49
- 4. Anotar scope_id = page.id
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** — se uma page tem filhas, e as filhas têm netas, TODAS devem ser trazidas. `get_page_children` retorna só 1 nível o agent DEVE fazer loop recursivo.
65
- > 2. **Local é flat** — todos os arquivos ficam soltos em `workspace/Input/{nome}/`, SEM subpastas por nível. A hierarquia do Confluence NÃO é replicada localmente.
66
- > 3. **Refletir 100% do externo** — tudo que existe no backend (pages, attachments) DEVE existir em `workspace/Input/{nome}/`. Se falta algo, o `/sf-load` está errado.
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 Confluence**:
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
- content = mcp__atlassian__confluence_get_page(page_id=page.id, convert_to_markdown=true)
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} // ← FLAT, sem subpastas
81
- registrar no load-log: page.id, page.title, page.version, sha256(content), filename
96
+ salvar content LIMPO em {targetDir}/{filename}
97
+ registrar no load-log
82
98
 
83
- // PASSO 3: Baixar attachments de CADA page (se include_attachments=true)
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 em attachments:
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} // ← FLAT junto com os .md
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 em children:
95
- result.push(child) // adiciona o filho
96
- collectAllPages(child.id, result) // desce nos netos, bisnetos, etc.
109
+ para cada child:
110
+ result.push(child)
111
+ collectAllPages(child.id, result) // recursão total
97
112
  ```
98
113
 
99
- **Exemplo concretoConfluence tem esta árvore:**
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
- **Resultado local em `workspace/Input/app_barbearia/` — TUDO FLAT:**
111
- ```
112
- workspace/Input/app_barbearia/
113
- ├── app_barbearia.md conteúdo da page raiz
114
- ├── requisitos_funcionais.md ← filho
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
- **Nenhuma subpasta.** Não importa quantos níveis a árvore do Confluence tem — local é flat.
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 (input já é local)
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 funcionais | 2 | 8b1c... | workspace/Input/app_barbearia/requisitos_funcionais.md | NOVO |
145
- | 491573 | Regras de negocio | 1 | 4d2a... | workspace/Input/app_barbearia/regras_de_negocio.md | NOVO |
146
- | 491574 | Neta da pagina | 1 | 7f3e... | workspace/Input/app_barbearia/neta_da_pagina.md | NOVO |
147
- | 491575 | Jornadas | 1 | 9c5b... | workspace/Input/app_barbearia/jornadas.md | NOVO |
148
- | 491576 | Stack tecnica | 1 | 2e8d... | workspace/Input/app_barbearia/stack_tecnica.md | NOVO |
149
- | att:diagrama.png | diagrama.png | | 7d2e... | workspace/Input/app_barbearia/diagrama.png | NOVO |
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
- - Antes de baixar, comparar hash atual com hash no log anterior
154
- - Se hash idêntico → status `INALTERADO`, não sobrescrever
155
- - Se hash diferente → status `MODIFICADO`, sobrescrever
156
- - Se não existia no log → status `NOVO`
157
- - Arquivo que existia no log mas sumiu do backend → status `REMOVIDO` (log only, não deleta local)
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` (ou seja, é um backend externo como Confluence):
162
- - Verificar se `.gitignore` contém o bloco `# SFW: workspace ignored`
163
- - Se NÃO contém, **adicionar** ao final do `.gitignore`:
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
- Insumos carregados em workspace/Input/{nome}/
174
+ Scope "{nome}" carregado:
185
175
 
186
- {N} arquivos ({X novos, Y modificados, Z inalterados})
187
- {M} attachments
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} ← se é bootstrap técnico (gera TRD)
191
- /sf-feature {nome} ← se é feature (gera PRD)
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 salvo em .ai/sf-load-log.md
199
+ Log: .ai/sf-load-log.md
194
200
  ```
195
201
 
196
202
  ## Notas
197
203
 
198
- - `/sf-load` é **idempotente** — rodar N vezes com mesmos insumos no backend
199
- não muda nada (hashes batem, status = INALTERADO)
200
- - Se o PM adicionar page `ajustes_v1` como filha do scope no Confluence,
201
- re-rodar `/sf-load {nome}` traz como NOVO automaticamente
202
- - O `/sf-load` **nunca modifica o backend** —
203
- - O `/sf-load` **nunca deleta arquivos locais** — se algo sumiu do backend,
204
- marca como REMOVIDO no log mas mantém o arquivo local
205
- - Attachments ficam flat junto com os .md (sem subpasta `attachments/`)
206
- - Nomes de arquivo são sanitizados: espaços `_`, caracteres especiais removidos
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, orientar setup |
215
- | Adapter não configurado | Parar, apontar pra SETUP.md |
216
- | MCP não disponível (Confluence) | Parar "Reinicie o Claude Code. MCP carrega no startup" |
217
- | Scope não encontrado no backend | Parar, listar scopes disponíveis se possível |
218
- | Auth error (401/403) | Parar "Verifique credenciais no .mcp.json" |
219
- | Page sem conteúdo | OK — salvar arquivo vazio, registrar no log |
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