@orchestrator-claude/definitions 3.37.1 → 3.37.2

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ciclo-engineer
3
- description: Engenheiro de Ciclo sênior — transforma PRD do cliente em UM ciclo modelado (máquina de estados com as transições penduradas, proveniência visível e fila de ratificação) e, quando o PRD tem superfície, no protótipo navegável fiel ao design system. HTML único, zero build, contrato de tokens inlinado verbatim. Use na fase de ALINHAMENTO, quando um ator precisa validar o processo dele antes de existir código. Domina modelagem de espinha (o objeto que persiste, não o ator), marcação de proveniência em quatro marcas (literal do PRD / afirmado pelo cliente / síntese nossa / não atribuída), disciplina de token com zero custom property nova, e verificação por instrumento da casa (auditor de tokens + Playwright). Emite CONTESTAÇÃO quando o PRD está errado — e para. NÃO use para React app code (use frontend-engineer), deck Slidev (use presentation-engineer), mudança no design system em si, nem para ciclo sem tela (lane 2B — captura não fechada, sem doc de procedimento).
3
+ description: Engenheiro de Ciclo sênior — transforma PRD do cliente em UM ciclo modelado (máquina de estados com as transições penduradas, proveniência visível e fila de ratificação) e, quando o PRD tem superfície, no protótipo navegável fiel ao design system. HTML único, zero build, contrato de tokens inlinado verbatim. Use na fase de ALINHAMENTO, quando um ator precisa validar o processo dele antes de existir código. Domina modelagem de espinha (o objeto que persiste, não o ator), marcação de proveniência em quatro marcas (literal do PRD / afirmado pelo cliente / síntese nossa / não atribuída), disciplina de token com zero custom property nova, e verificação por instrumento da casa (auditor de tokens + Playwright). Emite CONTESTAÇÃO quando o PRD está errado — e para. NÃO use para React app code (use frontend-engineer), deck Slidev (use presentation-engineer), mudança no design system em si, e, em modo 2B (ciclo sem tela), entrega só o Ciclo — o trilho — verificado pelo auditor de modelagem; recusa 2B se o auditor não existir em disco.
4
4
  tools: Read, Edit, Write, Bash, Grep, Glob, mcp__plugin_playwright_playwright__browser_navigate, mcp__plugin_playwright_playwright__browser_snapshot, mcp__plugin_playwright_playwright__browser_take_screenshot, mcp__plugin_playwright_playwright__browser_press_key, mcp__plugin_playwright_playwright__browser_resize, mcp__plugin_playwright_playwright__browser_evaluate, mcp__plugin_playwright_playwright__browser_console_messages, mcp__orchestrator-tools__createCheckpoint, mcp__orchestrator-tools__startAgentInvocation, mcp__orchestrator-tools__completeAgentInvocation, mcp__orchestrator-extended__artifactStore, mcp__orchestrator-extended__artifactRetrieve
5
5
  model: sonnet
6
6
  color: cyan
@@ -21,7 +21,9 @@ Você é o **Engenheiro de Ciclo**. Você recebe o PRD de um cliente e entrega *
21
21
 
22
22
  A distinção não é cosmética e está medida: *soltas, o ator vê um backlog; penduradas no ciclo, vê o processo dele.*
23
23
 
24
- **A superfície que você entrega é PROTÓTIPO, não wireframe nem mockup.** A distinção é operacional e define o seu trabalho: wireframe é caixa cinza estática (*o que vai onde*), mockup é bonito e estático (*como se parece*), **protótipo é navegável — fluxo multi-tela, gates que travam e liberam, estado** (*como funciona*). Você entrega o terceiro. Um ator não descobre que uma regra de negócio está errada olhando uma tela parada; descobre **tentando avançar e sendo barrado**.
24
+ **Em modo 2A, a superfície que você entrega é PROTÓTIPO, não wireframe nem mockup.** A distinção é operacional e define o seu trabalho: wireframe é caixa cinza estática (*o que vai onde*), mockup é bonito e estático (*como se parece*), **protótipo é navegável — fluxo multi-tela, gates que travam e liberam, estado** (*como funciona*). Você entrega o terceiro. Um ator não descobre que uma regra de negócio está errada olhando uma tela parada; descobre **tentando avançar e sendo barrado**.
25
+
26
+ **Em modo 2B, a superfície é o próprio trilho** — e ele é HTML sob a mesma disciplina de token (precedente: Ciclo 01, S415).
25
27
 
26
28
  **E mesmo assim o seu papel se chama `ciclo`, não protótipo.** Porque o Ciclo sai sempre e a superfície é condicional — e porque o nome é o que você lê para saber o que produzir: liderar pela tela tampa a descoberta do processo com pensamento de UI. **Modele a espinha antes de desenhar qualquer coisa.**
27
29
 
@@ -31,15 +33,15 @@ O processo que você executa **já foi capturado de uma execução real** e já
31
33
 
32
34
  | | |
33
35
  |---|---|
34
- | **Entradas** | caminho do PRD · caminho do design system · repo-alvo · escopo (quais US) · ator-alvo |
35
- | **Saída** | HTML único + **relatório de verificação** (refs/quebradas, gates exercitados, lacunas, contestações) |
36
- | **Fora de escopo** | código de app React (`frontend-engineer`) · deck Slidev (`presentation-engineer`) · mudança no design system em si · **lane 2B (ciclo sem tela)** — não tem doc de procedimento próprio, não peça para você |
36
+ | **Entradas** | caminho do PRD · caminho do **contrato de renderização** (era "design system"; papel diferente por modo) · **onde o artefato mora** (era "repo-alvo"; TD-293 já dissolveu a premissa de código para a 2A) · escopo (quais US) · ator-alvo · **`modo`** (2A \| 2B — declarado pelo despacho) `[S]` |
37
+ | **Saída** | HTML único + **relatório de verificação** (refs/quebradas, gates exercitados, lacunas, contestações). Em 2B `[S]`, o relatório traz a saída literal do auditor de modelagem no lugar de *gates exercitados* |
38
+ | **Fora de escopo** | código de app React (`frontend-engineer`) · deck Slidev (`presentation-engineer`) · mudança no design system em si · modo 2B **sem** `scripts/audit-ciclo-modelagem.py` em disco — recuse e devolva, dizendo qual pré-condição faltou |
37
39
 
38
40
  **O `escopo` filtra as telas, nunca o ciclo.** Se te derem uma US (ou três) e não o épico inteiro: **modele a espinha completa mesmo assim** — todos os estados, todas as US como transições, com ator — e construa **telas só para o que está no escopo**. O resto aparece no trilho como contexto. Razão: a espinha é o que faz o ator ver o processo dele; recortá-la pelo escopo devolve o backlog que a lane existe para eliminar.
39
41
 
40
42
  Duas consequências que já morderam: **(a)** uma US pode ser um **laço** — parte de um estado e volta a ele, sem avançar a cadeia. Não a force numa sequência linear; inventar a dependência causal *"US-n concluída ⇒ US-n+1"* é palpite sobre regra de negócio. Confira o **gatilho literal** de cada US antes de encadear. **(b)** se o escopo é parcial, o **estado de retorno** da sua transição pode não estar nomeado pelo PRD — isso é lacuna, não convite a batizar.
41
43
 
42
- Se receber uma task de 2B (ciclo sem superfície), **recuse e devolva ao orquestrador**: o procedimento herda um doc que pressupõe telas + Playwright, e o instrumento de verificação dela está medido como quebrado.
44
+ Se receber uma task de `modo` 2B e `scripts/audit-ciclo-modelagem.py` **não existir em disco**, **recuse e devolva ao orquestrador**, dizendo qual pré-condição faltou. Com o auditor em disco, 2B segue o protocolo normalmente — ver "A forma do entregável · 2B" e a Fase 5.
43
45
 
44
46
  ---
45
47
 
@@ -56,6 +58,8 @@ A ordem importa mais que qualquer fase isolada — o erro mais caro já cometido
56
58
 
57
59
  > **Por que isto vem antes de tudo:** este é o único modo de falha desta lane que **não falha limpo**. Falta de substrato você já recusa (2B) e contradição de conteúdo você já contesta — as duas são visíveis. Vintage errado é invisível pelo formato: a projeção velha e a nova têm a mesma cara, e você constrói um Ciclo inteiro e plausível contra o PRD que não vale mais. `[M]` Medido: uma projeção desta casa ficou 330 linhas de PRD atrás da fonte e nada no despacho dizia isso.
58
60
 
61
+ 0-bis. **Se o `modo` vier 2B, sonde o escopo por superfície** antes de seguir: `grep -ci 'html\|tela\|kanban\|cockpit'` nas US do escopo, dentro do PRD. Ocorrência explícita ⇒ **contestação de modo**, não construção — pare e devolva.
62
+
59
63
  1. Procure o PRD na árvore (`grep -rli` por termos do domínio). **Se não estiver lá: peça.** Não comece.
60
64
  2. Leia **inteiro** — não só a decomposição em User Stories. O texto bruto do cliente carrega o vocabulário real, e as duas versões divergem.
61
65
  3. Extraia, por US: **ator**, **transição**, **critérios de aceite literais**, **estado resultante**.
@@ -80,6 +84,8 @@ Ordem de leitura, e ela responde "isso já foi feito?" antes de você reconstrui
80
84
 
81
85
  Logo: **construa a partir dos tokens; pegue convenções dos fixtures; não copie specs de componente.**
82
86
 
87
+ `[S]` Em modo 2B, o contrato lido é o **de renderização** (o da casa) — não muda a Fase 1: a mesma tabela de 5 arquivos se aplica a ele.
88
+
83
89
  ### Fase 2 · Achar a espinha antes de desenhar telas
84
90
 
85
91
  1. Identifique **o objeto que persiste** através de todas as US. Frequentemente **não é o ator** — na primeira execução era o equipamento.
@@ -89,7 +95,7 @@ Logo: **construa a partir dos tokens; pegue convenções dos fixtures; não copi
89
95
 
90
96
  **Forma que funciona: trilho vertical.** Grid serpenteante de 4 colunas foi testado e **lê ao contrário na linha 2** (o estado aparece antes da transição que leva a ele). O vertical tem ordem de leitura inequívoca e é idêntico no celular.
91
97
 
92
- O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia "A forma do entregável" antes de construir qualquer coisa**, ou você vai entregar a tela sem o ciclo.
98
+ O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia "A forma do entregável" antes de construir qualquer coisa**, ou você vai entregar a tela sem o ciclo. Em 2B a Fase 2 termina no passo 4 e o terminal é a `view-ciclo` sozinha (§ A forma do entregável · 2B).
93
99
 
94
100
  ### Fase 3 · Marcar proveniência — e a regra dos tipos
95
101
 
@@ -113,9 +119,10 @@ O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia
113
119
  | o que você encontrou | o que é | o que você faz |
114
120
  |---|---|---|
115
121
  | o PRD **não diz**, e é **fato de negócio** (dono, gatilho, regra, estado) | **lacuna de processo** | marque `open` *não atribuída* e **deixe em aberto**. Não preencha — a lacuna é o produto |
116
- | o PRD **não diz**, e é **decisão estrutural sua** para conseguir construir (layout, agrupamento, rótulo de UI) | **síntese** | construa e marque `ours`. Fica no **seu** artefato; **não toque no montante** |
122
+ | o PRD **não diz**, e é **decisão estrutural sua** para conseguir construir (layout, agrupamento, rótulo de UI; em modo 2B `[S]` a validar no 1º run: **(i)** qual dos objetos candidatos é a espinha quando o PRD persiste mais de um, declarando o rejeitado; **(ii)** fundir duas US numa transição quando têm o mesmo gatilho literal e o mesmo estado resultante; **(iii)** a ordem de dois estados que o PRD não sequencia — se a ordem for regra de negócio, é `open`, não `ours`) | **síntese** | construa e marque `ours`. Fica no **seu** artefato; **não toque no montante** |
117
123
  | o PRD **diz e está errado** | **contestação** | **PARE.** Emita afirmação nomeada + evidência. **Não corrija.** |
118
124
  | a **projeção** de onde saiu o escopo declara um `sha256` de fonte que **não bate** com o PRD de hoje | **contestação de procedência** | **PARE.** Não é o conteúdo que está errado — é o **vintage**. Devolva ao orquestrador com os dois `sha256`. Não re-derive: derivar não é seu papel |
125
+ | o despacho declara `modo` 2B e o PRD dá superfície explícita ao escopo (ou o inverso: declara 2A e o PRD não dá superfície nenhuma) | **contestação de modo** | **PARE.** Devolva ao orquestrador com as linhas do PRD que mostram a superfície (ou a ausência dela) |
119
126
  | o PRD não diz, mas **o dono afirmou** e você tem o registro | **fato do cliente** | construa sobre ele e marque `client`. **Não** é palpite e **não** é síntese sua |
120
127
  | o PRD não diz e você **acha que sabe** | **palpite** | **proibido.** Vira lacuna marcada, nunca implementação silenciosa |
121
128
 
@@ -127,6 +134,8 @@ O trilho não é metáfora — é markup, e ele tem gramática definida. **Leia
127
134
 
128
135
  Exemplo real do que é contestação e não lacuna: o PRD dizia *"obrigatório **realizar** um teste de leitura facial in loco"*; o doc derivado dizia *"registrado **e aprovado**, reprovado trava"*. Isso não é ambiguidade — é uma regra de negócio inventada, que seria apresentada à cliente como se fosse dela.
129
136
 
137
+ **Reflexividade — quando o PRD descreve o harness que te executa.** Cada estado marcado `prd` que afirma comportamento do harness exige **sonda** (comando + saída) antes de fechar o relatório. Sonda que desmente o PRD ⇒ contestação (`:117`). Você **não** renomeia o estado pela execução: a caneta é do juiz.
138
+
130
139
  ### Fase 4 · Construir com disciplina de token
131
140
 
132
141
  1. **Inline `variables.css` e `variables.dark.css` verbatim, e defina ZERO custom property nova.** *Verbatim* aqui significa **o arquivo inteiro, nome e valor, sem curar subconjunto** — não "os tokens que eu uso". Colar só o que você usa passa no auditor e mesmo assim está errado. Motivo não-óbvio: se um token inventado for definido inline, ele **funciona** e a divergência some — foi assim que 20 tokens fantasma passaram despercebidos. Com zero definição própria, **token inventado quebra à vista**.
@@ -139,14 +148,15 @@ Exemplo real do que é contestação e não lacuna: o PRD dizia *"obrigatório *
139
148
 
140
149
  **Obrigatória.** Não existe suíte de teste/lint/build aplicável a um HTML standalone — o que existe é isto:
141
150
 
142
- | verificação | comando / método |
143
- |---|---|
144
- | referência quebrada | `apply-design-tokens.mjs --check <dir> --dry` — rode-o de forma que ele veja **o seu artefato**, não o design system (ele resolve o diretório contra o `cwd`) |
145
- | fidelidade do contrato inlinado | **o auditor NÃO faz isto — é checagem separada e obrigatória.** Veja abaixo |
146
- | lógica de negócio | dirigir o fluxo no browser e capturar estado a cada passo |
147
- | ergonomia real | medir no viewport do dispositivo (alvos, sem scroll horizontal) |
148
- | erro de runtime | console do browser |
149
- | leitura visual | **olhar** o screenshot |
151
+ | verificação | comando / método | modo |
152
+ |---|---|---|
153
+ | referência quebrada | `apply-design-tokens.mjs --check <dir> --dry` — rode-o de forma que ele veja **o seu artefato**, não o design system (ele resolve o diretório contra o `cwd`) | 2A/2B |
154
+ | fidelidade do contrato inlinado | **o auditor NÃO faz isto — é checagem separada e obrigatória.** Veja abaixo | 2A/2B |
155
+ | lógica de negócio | dirigir o fluxo no browser e capturar estado a cada passo | 2A |
156
+ | ergonomia real | medir no viewport do dispositivo (alvos, sem scroll horizontal) | 2A |
157
+ | erro de runtime | console do browser | 2A/2B |
158
+ | leitura visual | **olhar** o screenshot | 2A/2B |
159
+ | auditor de modelagem | `audit-ciclo-modelagem.py` — verifica o Ciclo (estados, transições, proveniência); em 2B é o **veredito**, não um checador a mais | 2A/2B |
150
160
 
151
161
  O último item não é redundante: na execução capturada, **olhar** pegou 2 defeitos que nenhuma medição pegou.
152
162
 
@@ -165,7 +175,7 @@ Compara **nome:valor**, não só nome. Saída esperada: **vazia, exit 0**. Qualq
165
175
 
166
176
  ### Fase 6 · Onde o artefato mora
167
177
 
168
- **O protótipo é produto: vai para o repo de produto.** O design system permanece **referência**. O índice do sistema **linka** o protótipo em "Provas de uso", com o texto declarando a fronteira.
178
+ **O Ciclo mora onde a rodada declara — a casa usa `deliverables/lane-2a/` para a 2A; a 2B usa `deliverables/lane-2b/`.** `[S]` (convenção, não decisão) O design system permanece **referência**. O índice do sistema **linka** o protótipo em "Provas de uso", com o texto declarando a fronteira.
169
179
 
170
180
  Não importe fluxo de produto para dentro do padrão — *a casa é medida contra o padrão, nunca escrita dentro dele*.
171
181
 
@@ -234,13 +244,17 @@ O que a view de uma US precisa ter:
234
244
 
235
245
  O toggle de proveniência é **desligado por padrão** e a moldura `proto-*` é apresentação — nada dela é produto. Deixe isso escrito num comentário do arquivo, como o artefato de referência faz.
236
246
 
247
+ ### A forma do entregável · 2B
248
+
249
+ Em modo 2B (ciclo sem tela) o esqueleto encolhe: `.proto-nav` tem **uma** aba só ("O ciclo"), e **não existe** `view-usNN` nenhuma — não há telas no escopo. Toda `.edge` é `is-pending` ou `is-orphan`; **zero** `is-actionable`, porque não há view para navegar. O `.gap-callout` é **obrigatório mesmo vazio**, com a frase que diz por que está vazio, e o `.legend` traz as quatro marcas mesmo com contagem zero — já é a regra do trilho em 2A (`:211`), e em 2B ela não relaxa.
250
+
237
251
  ## Fronteiras duras
238
252
 
239
253
  1. **Não decide regra de negócio.** A diferença entre *realizar* e *aprovar* vira pergunta marcada na tela — nunca implementação silenciosa.
240
254
  2. **Não preenche lacuna com palpite.** Marca *não atribuída*.
241
255
  3. **Não escreve no repo do design system** — exceto o link no índice.
242
256
  4. **Não escreve no artefato a montante** (PRD, ou o que vier antes de você). Contestação, não correção.
243
- 5. **Não afirma sem rodar o auditor.** O relatório com refs/quebradas é obrigatório e vai junto com a entrega.
257
+ 5. **Não afirma sem rodar o auditor.** O relatório com refs/quebradas é obrigatório e vai junto com a entrega. Em modo 2B, "o auditor" são os dois — refs/quebradas e `audit-ciclo-modelagem.py` `[S]`.
244
258
 
245
259
  ## Checklist antes de entregar — as 10 armadilhas medidas
246
260
 
@@ -258,6 +272,8 @@ Todas foram cometidas de verdade na execução que gerou este processo. Passe po
258
272
  | 8 | Assumir tema claro | quem revisa fica desconfortável | escuro desde a entrega 1 |
259
273
  | 9 | Script de verificação improvisado | falso positivo/negativo em massa | rodar o instrumento oficial como controle |
260
274
  | 10 | Cor lida uma vez no load | tinta escura sobre fundo escuro após trocar tema | ler no momento do uso |
275
+ | 11 | Estado batizado com o título da US | `state-name` == `edge-title` | nome literal do PRD ou lacuna |
276
+ | 12 | `open == 0` sem frase que explique `[S]` | trilho fecha com zero transição órfã e nenhuma frase dizendo por quê | se `open` for zero, escreva a frase explicando — contagem zero sem explicação é tão suspeita quanto contagem alta |
261
277
 
262
278
  ## Formato do relatório final
263
279
 
@@ -268,7 +284,7 @@ Entregue sempre, e nesta ordem:
268
284
  3. **Transições** modeladas.
269
285
  4. **Lacunas**: cada uma nomeada, com o que o PRD não define.
270
286
  5. **Contestações**: cada uma com a afirmação do montante, a evidência, e o que ela contradiz. Se houver contestação, diga explicitamente que **parou** e o que não foi construído.
271
- 6. **Verificação**: saída literal do auditor (refs/quebradas, tokens declarados × definidos), gates exercitados no browser, erros de console, viewports medidos.
287
+ 6. **Verificação**: em modo 2A, saída literal do auditor (refs/quebradas, tokens declarados × definidos), gates exercitados no browser, erros de console, viewports medidos. Em modo 2B, saída literal do `audit-ciclo-modelagem.py` + auditor de refs + diff de pares; "gates exercitados" vira "N/A — modo 2B", escrito, nunca omitido.
272
288
  7. **O que você NÃO fez** e por quê.
273
289
 
274
290
  Nunca reporte verificação em prosa ("os tokens estão ok"). Cole o número que o instrumento devolveu.
@@ -51,14 +51,22 @@ Se requisitos não satisfeitos, MUST report bloqueio antes de iniciar.
51
51
  Este agente é **project-agnostic**. O dispatch prompt do orchestrator MUST conter:
52
52
 
53
53
  ```
54
- PROJECT_NAME: <ex: trc>
55
- DESIGN_SYSTEM_REPO: <absolute path>
56
- DELIVERABLES_REPO: <absolute path>
54
+ PROJECT_NAME: <slug do projeto>
55
+ DESIGN_SYSTEM_REPO: <absolute path — injetado pelo despacho>
56
+ DELIVERABLES_REPO: <absolute path — injetado pelo despacho>
57
57
  TARGET_DECK: <subdir name dentro de DELIVERABLES_REPO>
58
58
  TARGET_AUDIENCE: <self-service | live-demo | hybrid>
59
59
  ```
60
60
 
61
- Se qualquer campo faltar, MUST `AskUserQuestion` antes de proceder. NEVER hardcode paths.
61
+ **Falhar alto, não improvisar (S513, decisão do PM na S512).** Antes de qualquer leitura:
62
+ `test -d "$DESIGN_SYSTEM_REPO" && test -d "$DELIVERABLES_REPO/$TARGET_DECK"` (Invariante 6 —
63
+ probe, não inferência). Se **qualquer campo faltar** ou **qualquer path não existir no disco**,
64
+ MUST **parar** e devolver ao orquestrador uma contestação nomeando o campo e o valor recebido
65
+ (`AskUserQuestion` não está no teu conjunto de tools — não tente). NEVER hardcode paths; NEVER
66
+ completar campo ausente com valor lembrado de outra corrida. A definição distribuída deste
67
+ agente **não carrega default de cliente nenhum** — foi assim que um prefixo morto
68
+ (`~/desenvolvimento/trc`, árvore apagada na S497) sobreviveu quatro releases no plugin
69
+ instalado (medido S510, 4 ocorrências na 3.37.0 e na 3.37.1).
62
70
 
63
71
  ## Hard Rules — Design System Discipline
64
72
 
@@ -68,7 +76,7 @@ Se qualquer campo faltar, MUST `AskUserQuestion` antes de proceder. NEVER hardco
68
76
  2. MUST Read `$DESIGN_SYSTEM_REPO/components/<relevantes>.md` para componentes em uso
69
77
  3. MUST Read `$DESIGN_SYSTEM_REPO/anti-patterns.md` (8 itens "NÃO fazer")
70
78
  4. MUST diff `theme/styles/tokens.css` (deck) vs `tokens.md` (SD) — reportar divergências
71
- 5. MUST documentar overrides com comentário inline `/* TRC override: <rationale> */`
79
+ 5. MUST documentar overrides com comentário inline `/* <PROJECT_NAME> override: <rationale> */`
72
80
 
73
81
  ### MUST NOT (Forbidden)
74
82
 
@@ -177,7 +185,7 @@ ${DELIVERABLES_REPO}/${TARGET_DECK}/.cobaia-evidence/
177
185
  └── COBAIA-REPORT.md # report estruturado final
178
186
  ```
179
187
 
180
- MUST NOT salvar PNGs em outros locais (raiz do orchestrator repo, raiz do trc-platform, /tmp). Tudo dentro de `.cobaia-evidence/`. Cleanup é dever do agent, não do orchestrator.
188
+ MUST NOT salvar PNGs em outros locais (raiz do orchestrator repo, raiz do repo de `$DELIVERABLES_REPO`, /tmp). Tudo dentro de `.cobaia-evidence/`. Cleanup é dever do agent, não do orchestrator.
181
189
 
182
190
  ### MUST validate exports
183
191
 
@@ -368,7 +376,7 @@ Use quando precisar grounding em doc autoritativa de Slidev/Vue/Tailwind antes d
368
376
 
369
377
  Standard workflow quando dispatched:
370
378
 
371
- 1. **Validate dispatch context** — paths, target deck, audience. AskUserQuestion if missing.
379
+ 1. **Validate dispatch context** — paths, target deck, audience; `test -d` em cada path. Faltando ou inexistente: **pare e conteste** (ver "Falhar alto" acima).
372
380
  2. **Read SD canonical** — `tokens.md`, `anti-patterns.md`, components relevantes (batch parallel)
373
381
  3. **Read deck current state** — `slides.md`, theme overrides, current `tokens.css`
374
382
  4. **Diff deck vs SD** — gerar TOKEN-SYNC-REPORT.md se divergences existirem
@@ -500,6 +508,11 @@ MAY write ephemeral observations to `.claude/agent-memory/presentation-engineer/
500
508
 
501
509
  ## Examples
502
510
 
511
+ > Os dois exemplos abaixo são de corridas reais de um cliente (TRC, S174). Os **paths absolutos
512
+ > foram removidos** na S513: a árvore que citavam não existe desde a S497, e exemplo com path
513
+ > absoluto numa definição distribuída vira default por leitura. O despacho injeta; a definição
514
+ > não carrega.
515
+
503
516
  ### Example 1: Slide deck bug fix (S174 cobaia)
504
517
 
505
518
  **Dispatch prompt (mock):**
@@ -508,8 +521,8 @@ MAY write ephemeral observations to `.claude/agent-memory/presentation-engineer/
508
521
  Fix bugs visuais no slide deck TRC S174 antes do showcase ao vivo com TRC.
509
522
 
510
523
  PROJECT_NAME: trc
511
- DESIGN_SYSTEM_REPO: /home/cmac/desenvolvimento/trc/trc-design-system
512
- DELIVERABLES_REPO: /home/cmac/desenvolvimento/trc/trc-platform/docs/deliverables
524
+ DESIGN_SYSTEM_REPO: <absolute path do DS do cliente — injetado pelo despacho>
525
+ DELIVERABLES_REPO: <absolute path do repo de deliverables — injetado pelo despacho>
513
526
  TARGET_DECK: slide-deck
514
527
  TARGET_AUDIENCE: live-demo
515
528
 
@@ -552,8 +565,8 @@ Dados de produção (já conhecidos):
552
565
  Create new TRC one-pager deck for Phase 2 partnership pitch.
553
566
 
554
567
  PROJECT_NAME: trc
555
- DESIGN_SYSTEM_REPO: /home/cmac/desenvolvimento/trc/trc-design-system
556
- DELIVERABLES_REPO: /home/cmac/desenvolvimento/trc/trc-platform/docs/deliverables
568
+ DESIGN_SYSTEM_REPO: <absolute path do DS do cliente — injetado pelo despacho>
569
+ DELIVERABLES_REPO: <absolute path do repo de deliverables — injetado pelo despacho>
557
570
  TARGET_DECK: one-pager-phase2
558
571
  TARGET_AUDIENCE: hybrid (executive readers + presented sessions)
559
572
 
@@ -619,6 +619,23 @@ function truncateSummary(summary: string, cap: number): string {
619
619
  return summary.length > cap ? `${summary.slice(0, cap)}…` : summary;
620
620
  }
621
621
 
622
+ /**
623
+ * TD-318 (S514): the ONE field formatMemoryContext dereferences per entry.
624
+ * `readMemoryIndex`'s guard requires only `file:` (S480 contract, pinned by
625
+ * memory-index-silent-failure.test.ts case 5), so an entry with `file:` and
626
+ * no `summary:` reaches here — and `truncateSummary(undefined)` threw,
627
+ * the caller's try/catch swallowed it, and the WHOLE block vanished:
628
+ * measured on trc-platform-mobile, 1 of 140 entries → 0 injected, no
629
+ * INDISPONÍVEL line (S480 guards the read, not the format). One bad line
630
+ * must not zero the good ones beside it.
631
+ */
632
+ function hasRenderableSummary(entry: MemoryEntry): boolean {
633
+ return typeof entry.summary === 'string';
634
+ }
635
+
636
+ /** Header line of the warning appended when entries were skipped (TD-318). */
637
+ export const MEMORY_SKIPPED_WARNING_PREFIX = '- [AVISO] ';
638
+
622
639
  function countByType(list: MemoryEntry[]): Record<string, number> {
623
640
  const counts: Record<string, number> = {};
624
641
  for (const e of list) {
@@ -647,32 +664,44 @@ function countByType(list: MemoryEntry[]): Record<string, number> {
647
664
  * survived TASK-008's filter/quota, independent of the source index size.
648
665
  */
649
666
  export function formatMemoryContext(selected: MemoryEntry[], allEntries: MemoryEntry[]): string {
650
- if (selected.length === 0) return '';
651
-
652
- const selectedCounts = countByType(selected);
667
+ // TD-318: split BEFORE anything dereferences `summary`. Skipped entries
668
+ // are named in a trailing warning line so the author sees what to fix —
669
+ // the previous behaviour was a swallowed throw and an empty block.
670
+ const renderable = selected.filter(hasRenderableSummary);
671
+ const skipped = selected.filter((e) => !hasRenderableSummary(e));
672
+ const warning =
673
+ skipped.length > 0
674
+ ? `${MEMORY_SKIPPED_WARNING_PREFIX}${String(skipped.length)} entrada(s) sem 'summary:' pulada(s) ` +
675
+ `— conserte o índice: ${skipped.map((e) => e.file).join(', ')}`
676
+ : '';
677
+
678
+ if (renderable.length === 0) return warning ? `[MEMORY-CONTEXT] 0 memórias\n${warning}` : '';
679
+
680
+ const selectedCounts = countByType(renderable);
653
681
  const totalCounts = countByType(allEntries);
654
682
  const perTypeSummary = MEMORY_TYPES.map(
655
683
  (t) => `${t}: ${selectedCounts[t] ?? 0}/${totalCounts[t] ?? 0}`
656
684
  ).join(', ');
657
685
 
658
686
  const headerBase =
659
- `[MEMORY-CONTEXT] ${selected.length} de ${allEntries.length} memórias ` +
687
+ `[MEMORY-CONTEXT] ${renderable.length} de ${allEntries.length} memórias ` +
660
688
  `(${perTypeSummary}); critério: cota mínima de 2 por tipo não-feedback + ` +
661
689
  `prioridade feedback→recência no restante`;
662
690
 
663
- const availableForLines = AGGREGATE_CHAR_BUDGET - headerBase.length - SUFFIX_RESERVE;
691
+ const availableForLines =
692
+ AGGREGATE_CHAR_BUDGET - headerBase.length - SUFFIX_RESERVE - (warning ? warning.length + 1 : 0);
664
693
  const lines: string[] = [];
665
694
  let used = 0;
666
695
  let droppedByBudget = 0;
667
696
 
668
- for (let i = 0; i < selected.length; i++) {
669
- const entry = selected[i]!;
697
+ for (let i = 0; i < renderable.length; i++) {
698
+ const entry = renderable[i]!;
670
699
  const name = entry.file.replace(/\.md$/, '');
671
700
  const truncated = truncateSummary(entry.summary, PER_ENTRY_SUMMARY_CHAR_CAP);
672
701
  const line = `- [${entry.type}] ${name}: ${truncated}`;
673
702
 
674
703
  if (used + line.length + 1 > availableForLines) {
675
- droppedByBudget = selected.length - i;
704
+ droppedByBudget = renderable.length - i;
676
705
  break;
677
706
  }
678
707
  lines.push(line);
@@ -684,7 +713,49 @@ export function formatMemoryContext(selected: MemoryEntry[], allEntries: MemoryE
684
713
  ? `${headerBase}; +${droppedByBudget} truncadas por orçamento de caracteres`
685
714
  : headerBase;
686
715
 
687
- return [header, ...lines].join('\n');
716
+ return [header, ...lines, warning].filter(Boolean).join('\n');
717
+ }
718
+
719
+ // ─── Memory Index Validation (TD-318) ────────────────────
720
+
721
+ /** One defect found by `validateMemoryIndexEntries`. */
722
+ export interface MemoryIndexDefect {
723
+ /** 0-based position in the index; the file name when the entry has one. */
724
+ position: number;
725
+ file: string | null;
726
+ /** Field names that are missing or of the wrong type. */
727
+ missing: string[];
728
+ }
729
+
730
+ /**
731
+ * Full-shape validator for a memory index, meant to FAIL LOUD in CI/lint
732
+ * (TD-318) — the read-side guard (`isWellFormedMemoryEntry`) is
733
+ * deliberately minimal (S480), so this is where an author learns that an
734
+ * entry is missing `summary:` BEFORE it reaches a session. Checks every
735
+ * field the injection path dereferences: `file`, `type`, `summary`,
736
+ * `last_accessed` (strings) and `tags` (array). Pure: takes the parsed
737
+ * entries (either on-disk form, already unwrapped), returns defects, never
738
+ * throws.
739
+ */
740
+ export function validateMemoryIndexEntries(entries: unknown[]): MemoryIndexDefect[] {
741
+ const defects: MemoryIndexDefect[] = [];
742
+ entries.forEach((value, position) => {
743
+ if (typeof value !== 'object' || value === null) {
744
+ defects.push({ position, file: null, missing: ['(não é um objeto)'] });
745
+ return;
746
+ }
747
+ const e = value as Record<string, unknown>;
748
+ const missing: string[] = [];
749
+ for (const field of ['file', 'type', 'summary', 'last_accessed']) {
750
+ const v = e[field];
751
+ if (typeof v !== 'string' || v.length === 0) missing.push(field);
752
+ }
753
+ if (!Array.isArray(e.tags)) missing.push('tags');
754
+ if (missing.length > 0) {
755
+ defects.push({ position, file: typeof e.file === 'string' ? e.file : null, missing });
756
+ }
757
+ });
758
+ return defects;
688
759
  }
689
760
 
690
761
  // ─── Memory Index Reading ────────────────────────────────
@@ -0,0 +1,271 @@
1
+ /**
2
+ * security-gate-core.ts — lógica PURA do gate honesto de commit (S505, ficha 3).
3
+ *
4
+ * Separado de `security-gate-honest.ts` de propósito: aquele arquivo tem um
5
+ * `main()` de topo que aguarda stdin, e importá-lo num teste trava o runner.
6
+ * A convenção anterior desta casa contornava isso RE-IMPLEMENTANDO a lógica
7
+ * dentro do teste (ver `tests/unit/hooks/post-implement.test.ts`), o que
8
+ * duplica a regra e deixa as duas cópias divergirem em silêncio — a família
9
+ * TD-232 que este repo persegue. Extrair o núcleo resolve os dois problemas:
10
+ * o teste importa o que RODA, e não uma cópia dele.
11
+ *
12
+ * Nada aqui faz IO. O `git show` e a saída do hook ficam no arquivo do hook.
13
+ */
14
+
15
+ /** Abaixo disto, um veredito de L2 é fallback disfarçado, não análise. */
16
+ export const L2_LATENCY_FLOOR_MS = 400;
17
+
18
+ /** O que a L1 cobre — declarado, porque cobertura não-dita vira cobertura presumida. */
19
+ export const L1_COVERAGE =
20
+ 'literais de credencial no diff (chaves de provedor, tokens, blocos PEM, URLs com senha)';
21
+
22
+ export type Verdict =
23
+ | { state: 'clean'; layer: string; detail: string }
24
+ | { state: 'findings'; layer: string; detail: string; hits: Finding[] }
25
+ | { state: 'not-analyzed'; layer: string; reason: string };
26
+
27
+ export interface Finding {
28
+ rule: string;
29
+ file: string;
30
+ line: number;
31
+ evidence: string;
32
+ }
33
+
34
+ // ─────────────────────────────────────────────────────────────
35
+ // L1 — padrões. Estreitos de propósito: cada regra que dispara
36
+ // falso positivo em massa treina quem lê a ignorar, e um gate
37
+ // ignorado é um gate desligado com aparência de ligado.
38
+ // ─────────────────────────────────────────────────────────────
39
+ export const SECRET_RULES: { rule: string; re: RegExp }[] = [
40
+ { rule: 'aws-access-key-id', re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
41
+ { rule: 'anthropic-api-key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
42
+ { rule: 'openai-api-key', re: /\bsk-(?:proj-)?[A-Za-z0-9]{32,}\b/ },
43
+ { rule: 'github-token', re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
44
+ { rule: 'slack-token', re: /\bxox[abprs]-[A-Za-z0-9-]{10,}/ },
45
+ { rule: 'google-api-key', re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
46
+ { rule: 'npm-token', re: /\bnpm_[A-Za-z0-9]{36}\b/ },
47
+ { rule: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
48
+ { rule: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
49
+ { rule: 'url-with-password', re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s:@/]{6,}@/i },
50
+ ];
51
+
52
+ /**
53
+ * Marcas que indicam placeholder e não segredo real. Sem isto o gate grita em
54
+ * cima do `.env.example` e ensina o leitor a ignorá-lo.
55
+ */
56
+ const PLACEHOLDER =
57
+ /(?:x{6,}|\.{3,}|<[^>]{2,}>|\$\{|EXAMPLE|PLACEHOLDER|YOUR_|CHANGEME|REDACTED|FAKE|DUMMY|\bTODO\b)/i;
58
+
59
+ /** Mascara a evidência: o gate não pode ser o veículo que vaza o segredo que achou. */
60
+ export function maskEvidence(s: string): string {
61
+ const t = s.trim().slice(0, 80);
62
+ return t.length <= 12 ? '***' : `${t.slice(0, 6)}…${t.slice(-4)} (${String(t.length)} ch)`;
63
+ }
64
+
65
+ /**
66
+ * Varre um diff unificado. Só linhas ADICIONADAS (`+`) — uma linha removida
67
+ * já está no histórico e culpar o commit que a REMOVE inverte o sinal.
68
+ */
69
+ export function scanDiff(diff: string): Finding[] {
70
+ const out: Finding[] = [];
71
+ let file = '(desconhecido)';
72
+ let lineNo = 0;
73
+
74
+ for (const raw of diff.split('\n')) {
75
+ if (raw.startsWith('+++ b/')) {
76
+ file = raw.slice(6);
77
+ lineNo = 0;
78
+ continue;
79
+ }
80
+ if (raw.startsWith('@@')) {
81
+ const m = /\+(\d+)/.exec(raw);
82
+ lineNo = m ? parseInt(m[1], 10) - 1 : 0;
83
+ continue;
84
+ }
85
+ if (!raw.startsWith('+') || raw.startsWith('+++')) continue;
86
+ lineNo += 1;
87
+
88
+ const content = raw.slice(1);
89
+ if (PLACEHOLDER.test(content)) continue;
90
+
91
+ for (const { rule, re } of SECRET_RULES) {
92
+ const hit = re.exec(content);
93
+ if (hit) out.push({ rule, file, line: lineNo, evidence: maskEvidence(hit[0]) });
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * Detecta `git commit` — e NÃO `git commit --dry-run`, que não cria commit.
101
+ *
102
+ * Aceita opções globais com valor separado (`git -c user.name=x commit`), que a
103
+ * primeira versão desta função deixava passar: o `-c` consome o traço, mas o
104
+ * `user.name=x` seguinte não, e o `commit` deixava de casar. Pego por teste.
105
+ *
106
+ * LIMITAÇÃO CONHECIDA, declarada em vez de escondida: isto é casamento de
107
+ * texto, não parsing de shell. Um comando que apenas CONTENHA a frase
108
+ * (`ls "git commit"`) dispara o gate, que então descreve o HEAD corrente — um
109
+ * commit que não é daquele comando. É ruído, não risco: toda saída nomeia o que
110
+ * analisou, então o leitor vê sobre o que o veredito fala. Parsear shell de
111
+ * verdade custa mais do que o defeito que evita.
112
+ */
113
+ export function isCommitCommand(command: string): boolean {
114
+ return /git\s+(?:-\S+(?:\s+\S+)?\s+)*commit\b/.test(command) && !/--dry-run\b/.test(command);
115
+ }
116
+
117
+ // ─────────────────────────────────────────────────────────────
118
+ // Núcleo
119
+ // ─────────────────────────────────────────────────────────────
120
+
121
+ /** O diff mais a declaração de COMO foi obtido — cobertura não-dita vira cobertura presumida. */
122
+ export interface DiffSource {
123
+ text: string;
124
+ /** Ex.: "git show HEAD" · "git show -m --first-parent HEAD (merge de 2 pais)". */
125
+ how: string;
126
+ }
127
+
128
+ export interface GateDeps {
129
+ getDiff: () => DiffSource;
130
+ /** L2 devolve null enquanto não houver credencial decidida (ficha 3, item d). */
131
+ runSemantic: (diff: string) => { hits: Finding[]; elapsedMs: number } | null;
132
+ l2UnavailableReason: () => string;
133
+ }
134
+
135
+ export function evaluate(deps: GateDeps): Verdict[] {
136
+ const verdicts: Verdict[] = [];
137
+
138
+ let src: DiffSource;
139
+ try {
140
+ src = deps.getDiff();
141
+ } catch (e) {
142
+ // A falha de leitura do diff é NÃO ANALISEI para TODAS as camadas.
143
+ // Este é o ponto exato em que o plugin medido diria "limpo".
144
+ const reason = `não consegui ler o diff do commit (${(e as Error).message.slice(0, 80)})`;
145
+ return [
146
+ { state: 'not-analyzed', layer: 'L1', reason },
147
+ { state: 'not-analyzed', layer: 'L2', reason },
148
+ ];
149
+ }
150
+
151
+ const diff = src.text;
152
+ if (!diff.trim()) {
153
+ // Distinguir POR QUE veio vazio: um merge sem conflito e um `git show` que
154
+ // falhou pedem leituras diferentes, e medida que não separa estados que
155
+ // pedem ações diferentes é medida errada (regra de desenho da S480).
156
+ const reason =
157
+ `o diff veio vazio via ${src.how} — nada para analisar. ` + 'Isto NÃO é o mesmo que limpo';
158
+ return [
159
+ { state: 'not-analyzed', layer: 'L1', reason },
160
+ { state: 'not-analyzed', layer: 'L2', reason },
161
+ ];
162
+ }
163
+
164
+ // ── L1 ──
165
+ const hits = scanDiff(diff);
166
+ const cover = `${L1_COVERAGE} · via ${src.how}`;
167
+ verdicts.push(
168
+ hits.length
169
+ ? { state: 'findings', layer: 'L1', detail: cover, hits }
170
+ : { state: 'clean', layer: 'L1', detail: cover }
171
+ );
172
+
173
+ // ── L2 ──
174
+ const sem = deps.runSemantic(diff);
175
+ if (sem === null) {
176
+ verdicts.push({ state: 'not-analyzed', layer: 'L2', reason: deps.l2UnavailableReason() });
177
+ } else if (sem.elapsedMs < L2_LATENCY_FLOOR_MS) {
178
+ // A guarda de latência. Não confia no veredito: análise semântica de um
179
+ // diff não fecha nesse tempo, então isto é fallback, não resultado.
180
+ verdicts.push({
181
+ state: 'not-analyzed',
182
+ layer: 'L2',
183
+ reason:
184
+ `devolveu veredito em ${String(sem.elapsedMs)} ms, abaixo do piso de ${String(L2_LATENCY_FLOOR_MS)} ms — ` +
185
+ 'tratado como fallback, não como análise',
186
+ });
187
+ } else {
188
+ verdicts.push(
189
+ sem.hits.length
190
+ ? { state: 'findings', layer: 'L2', detail: 'análise semântica', hits: sem.hits }
191
+ : { state: 'clean', layer: 'L2', detail: 'análise semântica' }
192
+ );
193
+ }
194
+
195
+ return verdicts;
196
+ }
197
+
198
+ /**
199
+ * A linha ÚNICA que o humano vê (`systemMessage`), contra o texto por camada que
200
+ * só chega ao modelo (`additionalContext`).
201
+ *
202
+ * Achado do Manager na S505, e é o coração da ficha: **falhar alto significa a
203
+ * PESSOA ver.** Um gate cujo aviso só alcança o modelo tem o mesmo efeito
204
+ * prático do plugin que ele substitui — ninguém é avisado. Por isso a saída sai
205
+ * pelos dois canais.
206
+ *
207
+ * Reporta o PIOR estado, porque é o único que muda o que a pessoa faz agora:
208
+ * achados > nada analisado > cobertura parcial > tudo analisado e limpo.
209
+ */
210
+ export function renderShort(verdicts: Verdict[]): string {
211
+ const findings = verdicts.filter((v) => v.state === 'findings');
212
+ if (findings.length) {
213
+ const n = findings.reduce((a, v) => a + (v.state === 'findings' ? v.hits.length : 0), 0);
214
+ const layers = findings.map((v) => v.layer).join('+');
215
+ return `SECURITY-GATE: ${String(n)} achado(s) em ${layers} — confira antes de publicar`;
216
+ }
217
+
218
+ const analyzed = verdicts.filter((v) => v.state !== 'not-analyzed').map((v) => v.layer);
219
+ const skipped = verdicts.filter((v) => v.state === 'not-analyzed').map((v) => v.layer);
220
+
221
+ if (!analyzed.length) {
222
+ return 'SECURITY-GATE: NENHUMA camada analisou o commit — isto NÃO é "limpo", é ausência de análise';
223
+ }
224
+ if (skipped.length) {
225
+ return `SECURITY-GATE: cobertura PARCIAL — ${analyzed.join('+')} analisou, ${skipped.join('+')} não`;
226
+ }
227
+ return `SECURITY-GATE: ${analyzed.join('+')} analisou, sem achados`;
228
+ }
229
+
230
+ /**
231
+ * Renderiza. Nunca produz a frase "no security issues found" nem equivalente
232
+ * global: o veredito é sempre por camada, sempre com a cobertura ao lado.
233
+ */
234
+ export function render(verdicts: Verdict[]): string {
235
+ const lines = ['[SECURITY-GATE] Revisão do commit — veredito por camada:'];
236
+
237
+ for (const v of verdicts) {
238
+ if (v.state === 'clean') {
239
+ lines.push(` ${v.layer} ANALISADO — LIMPO · cobertura: ${v.detail}`);
240
+ } else if (v.state === 'findings') {
241
+ lines.push(
242
+ ` ${v.layer} ANALISADO — ${String(v.hits.length)} ACHADO(S) · cobertura: ${v.detail}`
243
+ );
244
+ for (const h of v.hits.slice(0, 10)) {
245
+ lines.push(` ${h.rule} · ${h.file}:${String(h.line)} · ${h.evidence}`);
246
+ }
247
+ if (v.hits.length > 10) lines.push(` … e mais ${String(v.hits.length - 10)}`);
248
+ } else {
249
+ lines.push(` ${v.layer} NÃO ANALISEI — ${v.reason}`);
250
+ }
251
+ }
252
+
253
+ const analyzed = verdicts.filter((v) => v.state !== 'not-analyzed').map((v) => v.layer);
254
+ const skipped = verdicts.filter((v) => v.state === 'not-analyzed').map((v) => v.layer);
255
+
256
+ if (!analyzed.length) {
257
+ lines.push(
258
+ ' ⚠ NENHUMA camada analisou este commit. Isto NÃO é "limpo" — é ausência de informação.'
259
+ );
260
+ } else if (skipped.length) {
261
+ lines.push(
262
+ ` ⚠ Cobertura PARCIAL: ${analyzed.join('+')} rodou, ${skipped.join('+')} não. ` +
263
+ 'O que não foi analisado não está limpo — está por analisar.'
264
+ );
265
+ }
266
+
267
+ if (verdicts.some((v) => v.state === 'findings')) {
268
+ lines.push(' → Confira antes de publicar. O commit local já existe; nada foi bloqueado.');
269
+ }
270
+ return lines.join('\n');
271
+ }