raptor-aios 0.14.1 → 0.15.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 (41) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +14 -7
  3. package/dist/_core/dist/agents/prompt-builder.js +10 -0
  4. package/dist/_core/dist/design/asset-normalize.js +10 -2
  5. package/dist/_core/dist/design/downloader.js +38 -2
  6. package/dist/_core/dist/design/fetcher.js +35 -9
  7. package/dist/_core/dist/design/figma-rest.js +5 -2
  8. package/dist/_core/dist/design/mapper.js +2 -0
  9. package/dist/_core/dist/design/scaffold.js +36 -7
  10. package/dist/_core/dist/design/screens.js +18 -0
  11. package/dist/_core/dist/gates/builtin.js +32 -9
  12. package/dist/_core/dist/gates/design-gates.js +31 -8
  13. package/dist/_core/dist/gates/m7-gates.js +23 -5
  14. package/dist/_core/dist/jira/mapper.js +100 -2
  15. package/dist/_core/dist/models/checklist.js +10 -6
  16. package/dist/_core/dist/models/clarify.js +69 -1
  17. package/dist/_core/dist/models/impl-log.js +71 -0
  18. package/dist/_core/dist/prompts/resumption.js +16 -2
  19. package/dist/_core/dist/state/feature.js +1 -1
  20. package/dist/_core/dist/transport/mcp.js +1 -0
  21. package/dist/_core/package.json +1 -1
  22. package/dist/_core/templates/raptor.yml.hbs +0 -1
  23. package/dist/_core/templates/spec.md.hbs +14 -12
  24. package/dist/commands/approve.js +22 -8
  25. package/dist/commands/design/pull.js +9 -1
  26. package/dist/commands/design/sync.js +26 -5
  27. package/dist/commands/implement.js +71 -7
  28. package/dist/commands/new.js +34 -3
  29. package/dist/commands/taskstoissues.js +16 -5
  30. package/dist/shared/design.js +2 -1
  31. package/dist/shared/project.js +4 -4
  32. package/package.json +1 -1
  33. package/scripts/bash/common.sh +64 -23
  34. package/scripts/bash/create-new-feature.sh +11 -6
  35. package/scripts/bash/setup-plan.sh +3 -3
  36. package/scripts/check-naming.sh +16 -8
  37. package/scripts/powershell/common.ps1 +51 -9
  38. package/scripts/powershell/create-new-feature.ps1 +3 -1
  39. package/scripts/prepare-npm.mjs +1 -1
  40. package/templates/commands/implement.md +38 -9
  41. package/templates/commands/specify.md +8 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,85 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.15.0] - 2026-06-22
9
+
10
+ Tema do épico KAN-20: **determinismo e consentimento**. Tudo que antes "passava em
11
+ silêncio", "escrevia no card sem perguntar" ou "despejava o arquivo Figma inteiro"
12
+ agora é explícito, opt-in ou escopado por node.
13
+
14
+ ### Added
15
+
16
+ - **`--design-scope node|file`** (em `new`, `design sync`, `design pull`). Controla o
17
+ escopo da captura de telas/assets. Em `new` e `design sync` o default é `node` (só o
18
+ frame linkado pela URL); em `design pull` o default é `file` (inspeção do arquivo
19
+ inteiro). `--design-scope file` é o escape hatch para o comportamento legado (arquivo
20
+ todo). Uma ref COM node-id captura aquele node sob qualquer scope.
21
+ - **`raptor new --activate` / `--no-activate`.** `new` agora aponta
22
+ `.raptor/feature.json` para a feature recém-criada (default ligado). `--no-activate`
23
+ preserva a feature anterior.
24
+ - **`--sync-jira`** (em `approve`, `implement`, `taskstoissues`). Opt-in para escrever
25
+ de volta no card Jira (comentário de ciclo de vida + transição; em `taskstoissues`,
26
+ comentário-resumo dos issues criados). Sem a flag, só loga a ação pretendida.
27
+ - **`raptor implement --finalize`.** Reconstrói `impl-log.md` a partir dos eventos
28
+ `code.generated` do `audit.jsonl` quando as tasks foram fechadas sem `--task`.
29
+ Recupera traces de AC das tags `[AC-#]` do `tasks.md`; quando não há, deixa vazio
30
+ para o gate M7 cobrar (não fabrica pass). Respeita `--dry-run`.
31
+
32
+ ### Changed
33
+
34
+ - **`--design-fetch` agora é escopo por node-id por default** (era arquivo inteiro,
35
+ até 107 telas). Card sem node-id pula a captura com aviso acionável; a spec referencia
36
+ `assets-manifest.json` em vez de listar tudo. **Migração:** use `--design-scope file`
37
+ para o comportamento antigo.
38
+ - **`raptor new` ativa a feature criada por default.** `feature.json` é dobrado no
39
+ auto-commit para empilhar specs sem sujar a árvore. **Migração:** use `--no-activate`
40
+ se contava com a feature anterior continuar ativa.
41
+ - **Escrita no Jira é opt-in (`--sync-jira`)** em `approve`/`implement`/`taskstoissues`.
42
+ **Migração:** pipelines que esperavam status-sync automático precisam adicionar a flag.
43
+ - **`gate.design.fidelity` bloqueia por scorecards.** Avalia scorecards sempre que
44
+ existirem; retorna `skipped` (nunca mais `pass` falso) só quando não há scorecards
45
+ nem impl-log. **Migração:** projetos cujo fidelity passava verde sem evidência podem
46
+ ver o gate bloquear agora.
47
+ - **Gates M7 falham (não skipam) quando há tasks concluídas mas falta `impl-log.md`.**
48
+ Sem tasks concluídas continua `skipped`. A skill `/raptor-implement` foi reescrita
49
+ para registrar cada task via `raptor implement --task`.
50
+ - **Namespace de specs unificado em `.raptor/specs`.** `get_next_feature_number` deixa
51
+ de saltar numeração por varrer `specs/` legado (002 → 011); legado vira só fallback
52
+ de leitura. Espelhado no PowerShell.
53
+ - **`branch_prefix` removido do schema/`raptor.yml`** (dead config, 0 leituras).
54
+ **Migração:** pode remover do `raptor.yml` — não tinha efeito.
55
+ - **stderr do MCP silenciado por default** (`StdioClientTransport` usa `stderr:'ignore'`).
56
+ Reative diagnóstico com `RAPTOR_MCP_DEBUG=1`.
57
+ - **Assets do fetch isolados com `.gitignore` RAPTOR-managed + guarda de colisão** —
58
+ binários baixados não entram em versionamento por acidente; a captura nunca
59
+ sobrescreve arte já versionada.
60
+
61
+ ### Fixed
62
+
63
+ - **Falso negativo de cobertura de US eliminado** — `gate.spec.ready` normaliza IDs:
64
+ header `US-001` casa com `[US1]`.
65
+ - **Markers falsos não acusam mais** — detector ignora code-spans (`` `[PREENCHER]` ``)
66
+ e linhas de checklist negadoras; helper compartilhado em spec/plan/tasks.ready.
67
+ - **Checklist sem ID conta corretamente** — parser conta itens `[x]` mesmo sem `CK-#`
68
+ (contido em `status`; gates estruturados inalterados).
69
+ - **Path in-place no redesign** — `gate.tasks.ready` herda path por fase; reset por
70
+ heading preserva detecção de drift.
71
+ - **Hash stale agora orienta a correção** — `gate.plan/tasks.input_hash` sugere
72
+ `raptor resync <feature> --artifact plan|tasks`.
73
+ - **`clarify --apply` popula `## Clarifications`** (upsert das Q&A; CLI é dono único,
74
+ skill só produz `answers.json`), preserva frontmatter e não mascara marker real.
75
+ - **Wiki-markup do Jira → Markdown no seeding** — `flattenAdf` converte `{quote}`,
76
+ `{{...}}`, `||tabela||`, `hN.`, `{code}`/`{noformat}`, gateado por detecção de wiki
77
+ (não toca Markdown existente; preserva `5 * 4`).
78
+ - **Ator canônico único no audit** — `currentActor` fixa o `user` no `git user.email`
79
+ (fallback name → `USER`).
80
+ - **`HAS_GIT` correto no `--dry-run`** — `create-new-feature.sh` detecta git antes da
81
+ bifurcação dry-run. Skill `specify` passa `--short-name` (slug kebab-case).
82
+ - **Robustez do seeding de design** — tokens vazios direcionam ao MCP `get_variable_defs`
83
+ e o gate bloqueia `tokens.json` vazio (`radii` sempre presente); `gate.design.ready`
84
+ casa telas por `name` do frontmatter (tolerante a sufixo node-id); `design sync`
85
+ avisa (não apaga) telas órfãs da convenção antiga de slug.
86
+
8
87
  ## [0.14.1] - 2026-06-21
9
88
 
10
89
  ### Fixed
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
  [![npm](https://img.shields.io/npm/v/raptor-aios?color=ef4444&label=raptor-aios&logo=npm)](https://www.npmjs.com/package/raptor-aios)
8
8
  [![node](https://img.shields.io/badge/node-%E2%89%A5%2020-3c873a?logo=node.js&logoColor=white)](https://nodejs.org)
9
9
  [![platform](https://img.shields.io/badge/target-React%20Native-61dafb?logo=react&logoColor=black)](https://reactnative.dev)
10
- [![tests](https://img.shields.io/badge/tests-1481%20passing-22c55e)](#️-desenvolvimento-local)
11
- [![version](https://img.shields.io/badge/version-0.14.0-ef4444)](CHANGELOG.md)
10
+ [![tests](https://img.shields.io/badge/tests-1597%20passing-22c55e)](#️-desenvolvimento-local)
11
+ [![version](https://img.shields.io/badge/version-0.15.0-ef4444)](CHANGELOG.md)
12
12
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
13
13
 
14
14
  </div>
@@ -24,7 +24,7 @@ npm install -g raptor-aios
24
24
  cd meu-app-react-native
25
25
  raptor setup # 🧭 assistente guiado: IA/IDE, preset, Jira/Figma (ou 'raptor init' direto)
26
26
  raptor new login-biometrico -d "Permitir login com Face ID / digital"
27
- # → cria a feature E já troca para a branch feat/001-login-biometrico
27
+ # → cria a feature, forka a branch feat/001-login-biometrico do HEAD e a ativa
28
28
  ```
29
29
 
30
30
  ---
@@ -201,7 +201,7 @@ Isolamento e coerência **sem configurar nada** — o `init` já deixa tudo pron
201
201
 
202
202
  ### Uma branch por unidade de trabalho
203
203
 
204
- `raptor new` cria e troca para a branch certa, sempre ramificando de `main`:
204
+ `raptor new` cria e troca para a branch certa, **forkando do HEAD atual** (modelo empilhado, não de `main`): cada nova feature ramifica de onde você está, então specs se **empilham** umas sobre as outras. Ao final, a feature recém-criada vira a **ativa** em `.raptor/feature.json` (comandos seguintes a resolvem sem `--feature`); use `--no-activate` para manter a feature anterior ativa (`--activate` é o default).
205
205
 
206
206
  | Você roda | Branch criada |
207
207
  | -------------------------------------- | --------------------------------------------------- |
@@ -210,7 +210,7 @@ Isolamento e coerência **sem configurar nada** — o `init` já deixa tudo pron
210
210
  | `raptor new refactor-auth --kind=debt` | `debt/003-refactor-auth` |
211
211
  | `raptor new login --jira APP-1234` | `feat/004-login.app-1234` *(tipo inferido do Jira)* |
212
212
 
213
- Numeração sequencial global, prefixo por tipo (`feat`/`bugfix`/`debt`), e a chave do Jira no fim quando houver. Já vem ligado (`git.branch_per_feature`); desligue com `--no-branch` num comando ou `no_git` no projeto.
213
+ Numeração sequencial global, prefixo por tipo (`feat`/`bugfix`/`debt`), e a chave do Jira no fim quando houver. Já vem ligado (`git.branch_per_feature`); desligue com `--no-branch` num comando ou `no_git` no projeto. Por padrão a spec scaffoldada é **commitada** na própria branch (para o próximo `raptor new` empilhar em cima); `--no-commit` deixa a spec sem commit.
214
214
 
215
215
  ### Commits coerentes, sem travar seu fluxo
216
216
 
@@ -271,9 +271,11 @@ Gera checklists de qualidade temáticos (code quality, testes, segurança, perfo
271
271
 
272
272
  Executa as tarefas em ordem de fase, respeitando paralelização e (se exigido) TDD. Marca cada tarefa `[x]` ao concluir e registra a **evidência M7** (qual arquivo cobre qual critério de aceitação).
273
273
 
274
+ > ⚠️ **`impl-log.md` é inegociável.** Cada `raptor implement --task <id> --files … --acceptance …` atualiza o `impl-log.md` com as execuções, artefatos e traces. Se ele estiver **ausente após a implementação**, os gates M7 **bloqueiam** o `verify` (deixam de "pular" por arquivo inexistente e passam a falhar). Se o agente esqueceu de registrar tarefa por tarefa via `--task`, rode `raptor implement <feat> --finalize` para **reconstruir** o `impl-log.md` a partir dos eventos `code.generated` do `audit.jsonl` — traces de aceitação ausentes ficam vazios para o gate M7 cobrar (complete-os com `--amend --acceptance <AC-#>`).
275
+
274
276
  ### 🎫 `/raptor-taskstoissues`
275
277
 
276
- Converte as tarefas em issues de um tracker (GitHub via MCP, Jira via integração).
278
+ Converte as tarefas em issues de um tracker (GitHub via MCP, Jira via integração). Criar as issues é o propósito do comando, mas **escrever de volta no card pai** (comentário de resumo) é **opt-in via `--sync-jira`** — sem a flag, nada é mutado no ticket de origem.
277
279
 
278
280
  ### 🌿 `/raptor-commit-review`
279
281
 
@@ -521,6 +523,8 @@ raptor jira pull APP-1234 # importa a issue
521
523
  raptor new login --jira APP-1234 # semeia a spec a partir da issue
522
524
  ```
523
525
 
526
+ > 🔒 **Escrita no Jira é opt-in.** O `raptor new --jira` só **lê** o card. Os comandos do ciclo que poderiam escrever de volta no ticket de origem (`approve`, `implement`, `taskstoissues`) **só mutam o Jira** — comentário de lifecycle e transição opcional — quando você passa **`--sync-jira`**. Sem a flag, o Raptor apenas anuncia o que faria.
527
+
524
528
  > 📖 Como o card é capturado **por inteiro** (campos ricos, links de Figma) e redistribuído na spec — com os gates cobrando: veja [docs/jira-spec-enrichment.md](docs/jira-spec-enrichment.md).
525
529
 
526
530
  ### 🎨 Integração com Figma (design determinístico)
@@ -535,6 +539,8 @@ raptor design sync home # re-captura idempotente (nunca sobrescreve
535
539
 
536
540
  O provider **`figma-rest`** usa a REST API da Figma com um PAT (`${FIGMA_TOKEN}` no `raptor.yml`), é determinístico e roda em CI. A captura gera `design/tokens.json` (colors/typography/spacing/radii), `design/screens/<slug>.md` (frames de topo) e `design/assets-manifest.json` (assets baixados com SHA-256, guarda anti-SSRF). Quando há captura real, os prompts fazem o **flip REDISTRIBUIR**: o agente passa a *refinar e redistribuir* os artefatos em disco em vez de "olhar o Figma" — cobrado pelo `gate.design.ready`.
537
541
 
542
+ A captura no `raptor new` é **opt-in via `--design-fetch`** (default ligado): com o Figma conectado, ela puxa tokens/telas/assets e semeia `design/`; com `--no-design-fetch` apenas scaffolda o link, sem buscar. O **escopo** é controlado por `--design-scope`: `node` (default) captura **só o frame linkado** (o node-id na URL); `file` é uma escotilha de fuga que puxa o **arquivo Figma inteiro** (comportamento legado). O mesmo `--design-scope` vale para `raptor design sync`.
543
+
538
544
  > 🎯 **Fidelidade visual:** depois do `implement`, um loop garante que a tela **replica** o Figma (anatomia + nota por micro-região + `gate.design.fidelity`) — veja [🎯 Fidelidade visual (Figma → React Native)](#-fidelidade-visual-figma--react-native).
539
545
 
540
546
  > 📖 Detalhes, providers e limitações: [docs/design-figma-sync.md](docs/design-figma-sync.md) e [docs/design-gate.md](docs/design-gate.md).
@@ -557,7 +563,7 @@ O provider **`figma-rest`** usa a REST API da Figma com um PAT (`${FIGMA_TOKEN}`
557
563
  🎨 Figma/Design design connect · status · pull · sync · disconnect
558
564
  ```
559
565
 
560
- Rode `raptor <comando> --help` para os detalhes (flags, exemplos) de cada um.
566
+ Rode `raptor <comando> --help` para os detalhes (flags, exemplos) de cada um, ou veja a **[📖 Referência de flags (docs/cli-reference.md)](docs/cli-reference.md)** — flags relevantes e novas da v0.15.0, por comando, com defaults.
561
567
 
562
568
  ### 💬 Quando você erra um comando
563
569
 
@@ -622,6 +628,7 @@ Comece pelo **[📖 Glossário Canônico](docs/glossary.md)** — a referência
622
628
  | Documento | Sobre |
623
629
  | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
624
630
  | **[📖 docs/glossary.md](docs/glossary.md)** | Glossário canônico: conceitos, hierarquias, catálogo de 48 gates e de comandos CLI |
631
+ | [📖 docs/cli-reference.md](docs/cli-reference.md) | Referência de flags por comando (novas/relevantes da v0.15.0) com defaults |
625
632
  | [🧭 docs/setup-wizard.md](docs/setup-wizard.md) | Assistente de instalação `raptor setup`: onboarding, IA/IDE, preset, Jira/Figma |
626
633
  | [🛡️ docs/readiness-gates.md](docs/readiness-gates.md) | Gates de prontidão (`spec/plan/tasks.ready`) em detalhe |
627
634
  | [🎨 docs/design-figma-sync.md](docs/design-figma-sync.md) | Captura determinística do Figma (`figma-rest`): tokens/telas/assets/fontes + flip REDISTRIBUIR |
@@ -158,6 +158,9 @@ const PHASE_MAP = {
158
158
  "6. Se a feature tiver design (pasta `design/`): consuma os assets já catalogados em",
159
159
  " `design/assets-manifest.json` (baixados em `assets/{images,icons,fonts}`) e baixe via",
160
160
  " Figma MCP só o que faltar; referencie por caminho concreto (tags `[screen: X]` / `[asset: X]`).",
161
+ "7. PRESERVE o frontmatter existente de `tasks.md` ao reescrevê-lo — em especial o",
162
+ " campo `plan_hash` (e qualquer outro já presente). NÃO o remova nem o reescreva:",
163
+ " o gate de staleness o usa para detectar `tasks.md` desatualizado. (NÃO NEGOCIÁVEL)",
161
164
  ].join("\n"),
162
165
  },
163
166
  implement: {
@@ -215,6 +218,13 @@ const PHASE_MAP = {
215
218
  "",
216
219
  "### Saída esperada:",
217
220
  'Um arquivo JSON contendo um array de objetos `{ "label": string, "answer": string }`.',
221
+ "",
222
+ "### Regras de escrita (NÃO NEGOCIÁVEL):",
223
+ "1. Produza APENAS `answers.json`. NÃO edite `spec.md` — nem a seção",
224
+ " `## Clarifications` nem os marcadores inline.",
225
+ "2. A escrita no spec é determinística e exclusiva do CLI (`raptor clarify --apply`):",
226
+ " ele resolve os marcadores e popula `## Clarifications` a partir do seu `answers.json`.",
227
+ " Editar o spec aqui causaria dupla-escrita e regrediria o determinismo.",
218
228
  ].join("\n"),
219
229
  },
220
230
  analyze: {
@@ -7,6 +7,7 @@ export function normalizeImagesToAssets(renders, fills, opts = {}) {
7
7
  const renderType = classifyAssetType(fmt);
8
8
  const renderExt = extForFormat(fmt);
9
9
  const renderDir = assetDirFor(renderType);
10
+ const screen = opts.screen?.trim() ? opts.screen.trim() : undefined;
10
11
  for (const [nodeId, url] of Object.entries(renders)) {
11
12
  if (!url)
12
13
  continue;
@@ -18,7 +19,14 @@ export function normalizeImagesToAssets(renders, fills, opts = {}) {
18
19
  path = `${renderDir}/${base}.${renderExt}`;
19
20
  }
20
21
  seen.add(path);
21
- out.push({ name: human || base, type: renderType, path, url, figma_node_id: nodeId });
22
+ out.push({
23
+ name: human || base,
24
+ type: renderType,
25
+ path,
26
+ url,
27
+ figma_node_id: nodeId,
28
+ ...(screen ? { screen } : {}),
29
+ });
22
30
  }
23
31
  for (const [ref, url] of Object.entries(fills)) {
24
32
  if (!url)
@@ -31,7 +39,7 @@ export function normalizeImagesToAssets(renders, fills, opts = {}) {
31
39
  path = `assets/images/${base}.${ext}`;
32
40
  }
33
41
  seen.add(path);
34
- out.push({ name: base, type: "image", path, url });
42
+ out.push({ name: base, type: "image", path, url, ...(screen ? { screen } : {}) });
35
43
  }
36
44
  return out;
37
45
  }
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import { mkdirSync, writeFileSync } from "node:fs";
3
4
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
@@ -64,16 +65,31 @@ export async function downloadAll(pending, root, opts = {}) {
64
65
  if (isAbsolute(rel) || (within !== rootAbs && !within.startsWith(rootAbs + sep))) {
65
66
  throw new DownloadError(`asset path escapes project root: ${a.path}`, 0);
66
67
  }
68
+ if (isGitTracked(rootAbs, rel)) {
69
+ const w = `asset "${a.name}" colidiria com arquivo versionado ${rel} — não sobrescrito`;
70
+ if (opts.onWarn)
71
+ opts.onWarn(w);
72
+ else
73
+ process.stderr.write(`Warning: ${w}\n`);
74
+ results[i] = { ...base, status: "pending" };
75
+ continue;
76
+ }
67
77
  const destAbs = join(root, rel);
68
78
  const { hash } = await downloadAsset(a.url, destAbs, opts);
69
79
  results[i] = { ...base, hash, status: "downloaded" };
70
80
  }
71
81
  catch (err) {
72
82
  const msg = err instanceof Error ? err.message : String(err);
83
+ const where = assetWhere(a);
84
+ const oversized = err instanceof DownloadError && /exceeds max_bytes/.test(msg);
85
+ const advice = oversized
86
+ ? " — pending, fora do manifest; eleve design.assets.max_bytes ou rebaixe via MCP"
87
+ : "";
88
+ const line = `asset "${a.name}"${where} not downloaded: ${msg}${advice}`;
73
89
  if (opts.onWarn)
74
- opts.onWarn(`asset "${a.name}" not downloaded: ${msg}`);
90
+ opts.onWarn(line);
75
91
  else
76
- process.stderr.write(`Warning: asset "${a.name}" not downloaded: ${msg}\n`);
92
+ process.stderr.write(`Warning: ${line}\n`);
77
93
  results[i] = { ...base, status: "pending" };
78
94
  }
79
95
  }
@@ -81,6 +97,26 @@ export async function downloadAll(pending, root, opts = {}) {
81
97
  await Promise.all(Array.from({ length: Math.min(concurrency, pending.length) }, worker));
82
98
  return results;
83
99
  }
100
+ function isGitTracked(rootAbs, rel) {
101
+ try {
102
+ const r = spawnSync("git", ["ls-files", "--error-unmatch", "--", rel], {
103
+ cwd: rootAbs,
104
+ stdio: "ignore",
105
+ });
106
+ return r.status === 0;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ function assetWhere(a) {
113
+ const parts = [];
114
+ if (a.screen)
115
+ parts.push(`screen: ${a.screen}`);
116
+ if (a.figma_node_id)
117
+ parts.push(`node ${a.figma_node_id}`);
118
+ return parts.length ? ` (${parts.join(", ")})` : "";
119
+ }
84
120
  function assertSafeAssetUrl(url, allowHttp) {
85
121
  let u;
86
122
  try {
@@ -6,6 +6,16 @@ function warn(opts, message) {
6
6
  else
7
7
  process.stderr.write(`Warning: ${message}\n`);
8
8
  }
9
+ function scopeSkipWarning(url) {
10
+ return (`ref sem node-id (link sem ?node-id=); telas/assets não capturados — ` +
11
+ `rode com --design-scope file para o arquivo inteiro, ou linke o frame no card: ${url}`);
12
+ }
13
+ function warnOnce(opts, seen, url) {
14
+ if (seen.has(url))
15
+ return;
16
+ seen.add(url);
17
+ warn(opts, scopeSkipWarning(url));
18
+ }
9
19
  export async function fetchDesignData(refs, client, opts = {}) {
10
20
  const varMap = {};
11
21
  for (const ref of refs) {
@@ -30,10 +40,11 @@ export async function fetchDesignData(refs, client, opts = {}) {
30
40
  }
31
41
  }
32
42
  const tokens = normalizeVariablesToTokens(varMap, opts.source);
33
- const screens = await captureScreens(refs, client, opts);
43
+ const scopeWarned = new Set();
44
+ const screens = await captureScreens(refs, client, opts, scopeWarned);
34
45
  const assets = [];
35
46
  if (opts.root) {
36
- assets.push(...(await captureAssets(refs, client, tokens.typography, opts)));
47
+ assets.push(...(await captureAssets(refs, client, tokens.typography, opts, scopeWarned)));
37
48
  }
38
49
  return {
39
50
  tokens,
@@ -42,12 +53,17 @@ export async function fetchDesignData(refs, client, opts = {}) {
42
53
  libraries: [...libraries],
43
54
  };
44
55
  }
45
- async function captureScreens(refs, client, opts) {
56
+ async function captureScreens(refs, client, opts, scopeWarned) {
46
57
  if (!client.captureScreens)
47
58
  return [];
59
+ const scope = opts.scope ?? "node";
48
60
  const out = [];
49
61
  const seen = new Set();
50
62
  for (const ref of refs) {
63
+ if (scope === "node" && !ref.nodeId) {
64
+ warnOnce(opts, scopeWarned, ref.url);
65
+ continue;
66
+ }
51
67
  try {
52
68
  for (const s of await client.captureScreens(ref.fileKey, ref.nodeId)) {
53
69
  const key = s.nodeId
@@ -65,7 +81,7 @@ async function captureScreens(refs, client, opts) {
65
81
  }
66
82
  return out;
67
83
  }
68
- async function captureAssets(refs, client, typography, opts) {
84
+ async function captureAssets(refs, client, typography, opts, scopeWarned) {
69
85
  const root = opts.root;
70
86
  const format = opts.assets?.format ?? "svg";
71
87
  const scale = opts.assets?.scale ?? 2;
@@ -73,8 +89,13 @@ async function captureAssets(refs, client, typography, opts) {
73
89
  const fetchImpl = opts.deps?.fetchImpl;
74
90
  const pending = [];
75
91
  const seenPaths = new Set();
92
+ const scope = opts.scope ?? "node";
76
93
  if (client.captureAssets) {
77
94
  for (const ref of refs) {
95
+ if (scope === "node" && !ref.nodeId) {
96
+ warnOnce(opts, scopeWarned, ref.url);
97
+ continue;
98
+ }
78
99
  try {
79
100
  const found = await client.captureAssets(ref.fileKey, ref.nodeId, { format, scale });
80
101
  for (const p of found) {
@@ -100,8 +121,15 @@ async function captureAssets(refs, client, typography, opts) {
100
121
  for (const e of entries) {
101
122
  if (e.status === "downloaded")
102
123
  out.push(e);
103
- else
104
- warn(opts, `asset "${e.name}" could not be downloaded — left pending (not catalogued)`);
124
+ else {
125
+ const where = [
126
+ e.screen ? `screen: ${e.screen}` : "",
127
+ e.figma_node_id ? `node ${e.figma_node_id}` : "",
128
+ ]
129
+ .filter(Boolean)
130
+ .join(", ");
131
+ warn(opts, `asset "${e.name}"${where ? ` (${where})` : ""} could not be downloaded — left pending (not catalogued)`);
132
+ }
105
133
  }
106
134
  }
107
135
  const fontEntries = await resolveFonts(typography, {
@@ -183,9 +211,7 @@ export function normalizeVariablesToTokens(map, source) {
183
211
  break;
184
212
  }
185
213
  }
186
- const tokens = { colors, typography, spacing };
187
- if (radii.length)
188
- tokens.radii = radii;
214
+ const tokens = { colors, typography, spacing, radii };
189
215
  if (source) {
190
216
  tokens.source = {
191
217
  provider: source.provider,
@@ -62,6 +62,7 @@ export function makeFigmaRest(token, opts = {}) {
62
62
  const scale = assetOpts.scale ?? 2;
63
63
  let renders = {};
64
64
  let scopedRefs = null;
65
+ let screen;
65
66
  if (nodeId) {
66
67
  const q = `ids=${encodeURIComponent(nodeId)}&format=${encodeURIComponent(format)}&scale=${scale}`;
67
68
  const data = await getJson(`/v1/images/${encodeURIComponent(fileKey)}?${q}`);
@@ -70,7 +71,9 @@ export function makeFigmaRest(token, opts = {}) {
70
71
  }
71
72
  renders = parseImagesResponse(data);
72
73
  try {
73
- scopedRefs = collectImageRefs(await readNodes(fileKey, nodeId));
74
+ const nodesDoc = await readNodes(fileKey, nodeId);
75
+ scopedRefs = collectImageRefs(nodesDoc);
76
+ screen = screensFromNodes(nodesDoc, fileKey)[0]?.name;
74
77
  }
75
78
  catch {
76
79
  scopedRefs = new Set();
@@ -88,7 +91,7 @@ export function makeFigmaRest(token, opts = {}) {
88
91
  const refs = scopedRefs;
89
92
  fills = Object.fromEntries(Object.entries(fills).filter(([ref]) => refs.has(ref)));
90
93
  }
91
- return normalizeImagesToAssets(renders, fills, { format });
94
+ return normalizeImagesToAssets(renders, fills, { format, ...(screen ? { screen } : {}) });
92
95
  },
93
96
  async captureScreens(fileKey, nodeId) {
94
97
  if (!fileKey)
@@ -1,4 +1,5 @@
1
1
  import { seededTokensCount } from "./fetcher.js";
2
+ import { isTokensEmpty } from "./tokens.js";
2
3
  export function mapDesignToSpecContext(refs, capture) {
3
4
  const designRefs = refs.map((r) => ({
4
5
  url: r.url,
@@ -20,6 +21,7 @@ export function mapDesignToSpecContext(refs, capture) {
20
21
  return {
21
22
  ...base,
22
23
  designSeeded: true,
24
+ seededTokensEmpty: isTokensEmpty(t),
23
25
  seededTokensCount: seededTokensCount(t),
24
26
  seededScreens: capture.screens.map((s) => ({
25
27
  name: s.name,
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { isTokensEmpty, parseDesignTokens } from "./tokens.js";
4
4
  import { parseAssetManifest } from "./manifest.js";
@@ -14,6 +14,7 @@ const EMPTY_TOKENS = {
14
14
  colors: [],
15
15
  typography: [],
16
16
  spacing: [],
17
+ radii: [],
17
18
  };
18
19
  function writeIfMissing(path, content) {
19
20
  if (existsSync(path))
@@ -34,8 +35,11 @@ by the agent through its Figma MCP tools and enforced by \`gate.design.ready\`.
34
35
  ${links}
35
36
 
36
37
  ## Files
37
- - \`tokens.json\` — design tokens (colors, typography, spacing). Fill via
38
- \`get_variable_defs\`.
38
+ - \`tokens.json\` — design tokens (colors, typography, spacing, radii). Fill via
39
+ \`get_variable_defs\`. NOTE: the deterministic capture reads Figma *Variables*
40
+ (Enterprise-plan REST) and falls back to published *styles*; a file with
41
+ neither yields an EMPTY tokens.json — populate it via the Figma MCP
42
+ \`get_variable_defs\` (it can read what the REST capture could not).
39
43
  - \`screens/<name>.md\` — one file per screen, with frontmatter (name, nodeId) and
40
44
  a short spec. Create from \`get_design_context\` / \`get_screenshot\`.
41
45
  - \`assets-manifest.json\` — catalogue of every downloaded asset
@@ -77,8 +81,34 @@ export function writeDesignScaffold(featureDir, root, refs) {
77
81
  created.push(`${dir}/.gitkeep`);
78
82
  }
79
83
  }
84
+ if (ensureAssetGitignore(root)) {
85
+ created.push(".gitignore");
86
+ }
80
87
  return created;
81
88
  }
89
+ const GITIGNORE_BEGIN = "# RAPTOR design assets";
90
+ const GITIGNORE_END = "# /RAPTOR design assets";
91
+ function ensureAssetGitignore(root) {
92
+ try {
93
+ const gitignorePath = join(root, ".gitignore");
94
+ const existing = existsSync(gitignorePath)
95
+ ? readFileSync(gitignorePath, "utf8")
96
+ : "";
97
+ if (existing.includes(GITIGNORE_BEGIN))
98
+ return false;
99
+ const lines = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
100
+ const missing = ASSET_DIRS.filter((d) => !lines.has(d) && !lines.has(`/${d}`));
101
+ if (missing.length === 0)
102
+ return false;
103
+ const block = (existing.length && !existing.endsWith("\n") ? "\n" : "") +
104
+ `${GITIGNORE_BEGIN}\n${missing.join("\n")}\n${GITIGNORE_END}\n`;
105
+ appendFileSync(gitignorePath, block);
106
+ return true;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
82
112
  function writeJson(path, data) {
83
113
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
84
114
  }
@@ -117,11 +147,10 @@ export function seedDesignArtifacts(featureDir, root, capture, refs) {
117
147
  const used = new Set();
118
148
  for (const screen of capture.screens) {
119
149
  const base = screenSlug(screen);
120
- let slug = base;
121
- let n = 1;
150
+ let slug = screen.nodeId ? `${base}-${slugify(screen.nodeId)}` : base;
151
+ let n = 2;
122
152
  while (used.has(slug)) {
123
- const suffix = n === 1 && screen.nodeId ? slugify(screen.nodeId) : String(n);
124
- slug = `${base}-${suffix}`;
153
+ slug = `${base}-${n}`;
125
154
  n++;
126
155
  }
127
156
  used.add(slug);
@@ -177,6 +177,24 @@ export function pageIdsFromMetadata(data) {
177
177
  export function screenSlug(screen) {
178
178
  return slugify(screen.name) || slugify(screen.nodeId ?? "screen") || "screen";
179
179
  }
180
+ export function orphanScreenStems(presentStems, screens) {
181
+ const present = new Set(presentStems);
182
+ const suffixedBases = new Set();
183
+ const bareBases = new Set();
184
+ for (const screen of screens) {
185
+ if (screen.nodeId)
186
+ suffixedBases.add(screenSlug(screen));
187
+ else
188
+ bareBases.add(screenSlug(screen));
189
+ }
190
+ const orphans = [];
191
+ for (const base of suffixedBases) {
192
+ if (present.has(base) && !bareBases.has(base) && !orphans.includes(base)) {
193
+ orphans.push(base);
194
+ }
195
+ }
196
+ return orphans;
197
+ }
180
198
  function bodyName(name) {
181
199
  return (name
182
200
  .replace(/[\r\n]+/g, " ")
@@ -6,7 +6,7 @@ import { extractCoreBlock } from "../constitution/core.js";
6
6
  import { parsePlan } from "../models/plan.js";
7
7
  import { parseSpec } from "../models/spec.js";
8
8
  import { parseTaskList, parseTasks } from "../models/tasks.js";
9
- import { detectClarificationMarkers, stripHtmlComments, } from "../models/clarify.js";
9
+ import { detectClarificationMarkers, hasUnresolvedPlaceholder, stripHtmlComments, } from "../models/clarify.js";
10
10
  import { gateBranchNaming } from "./branch-gate.js";
11
11
  function extractIdsByPattern(source, re) {
12
12
  const seen = new Set();
@@ -418,11 +418,11 @@ export const gateSpecReady = {
418
418
  };
419
419
  }
420
420
  const problems = [];
421
- if (/\[PREENCHER\b/i.test(body)) {
421
+ if (hasUnresolvedPlaceholder(body, /\[PREENCHER\b/i)) {
422
422
  problems.push("unfilled [PREENCHER] placeholder(s) remain");
423
423
  }
424
- const storyIds = new Set([...body.matchAll(/\bUS-?(\d+)\b/g)].map((m) => `US${m[1]}`));
425
- const coveredStories = new Set([...body.matchAll(/covers\b[^.\n]*?\bUS-?(\d+)/gi)].map((m) => `US${m[1]}`));
424
+ const storyIds = new Set([...body.matchAll(/\bUS-?(\d+)\b/g)].map((m) => `US${Number(m[1])}`));
425
+ const coveredStories = new Set([...body.matchAll(/covers\b[^.\n]*?\bUS-?(\d+)/gi)].map((m) => `US${Number(m[1])}`));
426
426
  const uncovered = [...storyIds].filter((s) => !coveredStories.has(s));
427
427
  if (storyIds.size > 0 && uncovered.length > 0) {
428
428
  problems.push(`user stor${uncovered.length > 1 ? "ies" : "y"} without acceptance coverage: ${uncovered.join(", ")}`);
@@ -523,7 +523,11 @@ export const gatePlanInputHash = {
523
523
  level: this.level,
524
524
  article: this.article,
525
525
  status: "fail",
526
- message: `stale: plan declares spec_hash=${plan.data.spec_hash.slice(0, 20)}…, current spec hash=${spec.hash.slice(0, 20)}…`,
526
+ message: `stale: plan declares spec_hash=${plan.data.spec_hash.slice(0, 20)}…, current spec hash=${spec.hash.slice(0, 20)}…\n` +
527
+ ` spec.md changed after plan.md was generated (likely a clarify/re-approve). ` +
528
+ `If the edit was intentional, run 'raptor resync <feature> --artifact plan' to re-bless plan.md's spec_hash, ` +
529
+ `then re-run 'raptor verify' (and re-approve if the change is material). ` +
530
+ `Otherwise regenerate plan.md with 'raptor plan <feature>'.`,
527
531
  details: {
528
532
  declared: plan.data.spec_hash,
529
533
  current: spec.hash,
@@ -600,7 +604,11 @@ export const gateTasksInputHash = {
600
604
  level: this.level,
601
605
  article: this.article,
602
606
  status: "fail",
603
- message: `stale: tasks declares plan_hash=${tasks.data.plan_hash.slice(0, 20)}…, current plan hash=${plan.hash.slice(0, 20)}…`,
607
+ message: `stale: tasks declares plan_hash=${tasks.data.plan_hash.slice(0, 20)}…, current plan hash=${plan.hash.slice(0, 20)}…\n` +
608
+ ` plan.md changed after tasks.md was generated (likely a clarify/re-approve upstream). ` +
609
+ `If the edit was intentional, run 'raptor resync <feature> --artifact tasks' to re-bless tasks.md's plan_hash, ` +
610
+ `then re-run 'raptor verify' (and re-approve if the change is material). ` +
611
+ `Otherwise regenerate tasks.md with 'raptor tasks <feature>'.`,
604
612
  details: {
605
613
  declared: tasks.data.plan_hash,
606
614
  current: plan.hash,
@@ -911,7 +919,7 @@ export const gatePlanReady = {
911
919
  if (detectClarificationMarkers(body).length > 0) {
912
920
  problems.push("unresolved [NEEDS CLARIFICATION] marker(s) remain");
913
921
  }
914
- if (/\[PREENCHER\b/i.test(stripped)) {
922
+ if (hasUnresolvedPlaceholder(body, /\[PREENCHER\b/i)) {
915
923
  problems.push("unfilled [PREENCHER] placeholder(s) remain");
916
924
  }
917
925
  if (/\bStatus:\s*`?pending`?/i.test(stripped) || /\|\s*pending\s*\|/i.test(stripped)) {
@@ -1010,12 +1018,22 @@ export const gateTasksReady = {
1010
1018
  if (detectClarificationMarkers(body).length > 0) {
1011
1019
  problems.push("unresolved [NEEDS CLARIFICATION] marker(s) remain");
1012
1020
  }
1013
- if (/\[PREENCHER\b/i.test(stripped)) {
1021
+ if (hasUnresolvedPlaceholder(body, /\[PREENCHER\b/i)) {
1014
1022
  problems.push("unfilled [PREENCHER] placeholder(s) remain");
1015
1023
  }
1024
+ const PHASE_HEADING_RE = /^#{2,}\s/;
1025
+ const bodyLines = body.split("\n");
1026
+ const lineToPhase = new Array(bodyLines.length);
1027
+ let phaseBucket = 0;
1028
+ for (let i = 0; i < bodyLines.length; i++) {
1029
+ if (PHASE_HEADING_RE.test(bodyLines[i]))
1030
+ phaseBucket++;
1031
+ lineToPhase[i] = phaseBucket;
1032
+ }
1016
1033
  const noUS = [];
1017
1034
  const noAC = [];
1018
1035
  const noFile = [];
1036
+ const phaseHasPath = new Set();
1019
1037
  for (const it of items) {
1020
1038
  if (META_TASK_RE.test(it.raw))
1021
1039
  continue;
@@ -1023,8 +1041,13 @@ export const gateTasksReady = {
1023
1041
  noUS.push(it.id);
1024
1042
  if (!/\[AC-\d+\]/i.test(it.raw))
1025
1043
  noAC.push(it.id);
1026
- if (!FILE_RE.test(it.raw))
1044
+ const phase = lineToPhase[it.lineIndex] ?? 0;
1045
+ if (FILE_RE.test(it.raw)) {
1046
+ phaseHasPath.add(phase);
1047
+ }
1048
+ else if (!phaseHasPath.has(phase)) {
1027
1049
  noFile.push(it.id);
1050
+ }
1028
1051
  }
1029
1052
  if (noUS.length > 0) {
1030
1053
  problems.push(`task(s) without a [US#] (orphan, not [meta]): ${noUS.join(", ")}`);