@andespindola/crow-skills 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Copyright (c) 2026 Anderson Espíndola. Todos os direitos reservados.
2
+
3
+ Software proprietário. Este pacote é distribuído publicamente para uso, não para
4
+ cópia. O código-fonte não é aberto: a sua presença no pacote é uma necessidade de
5
+ execução, não uma concessão de direitos sobre ele.
6
+
7
+ ## O que você pode fazer
8
+
9
+ 1. Instalar e executar o `crow-skills` nos seus próprios computadores, sem limite de
10
+ máquinas, para o seu uso pessoal ou da sua organização.
11
+ 2. Deixar que o CLI instale, nos seus computadores, as cópias das skills que ele
12
+ gerencia, e usar essas skills com os seus agentes.
13
+ 3. Configurar, adaptar localmente e desinstalar o que o CLI gravou na sua máquina.
14
+
15
+ ## O que você não pode fazer
16
+
17
+ 1. Redistribuir este pacote, no todo ou em parte, por qualquer meio — incluindo
18
+ republicar em registro de pacotes, espelhar, empacotar dentro de outro produto ou
19
+ disponibilizar para download.
20
+ 2. Criar ou distribuir obra derivada, incluindo traduções, adaptações e reescritas das
21
+ skills, do catálogo ou da documentação.
22
+ 3. Extrair o conteúdo das skills, do catálogo ou da documentação para treinar, povoar,
23
+ alimentar ou compor outro produto, biblioteca, modelo ou conjunto de dados.
24
+ 4. Fazer engenharia reversa, descompilar, desmontar ou de outro modo analisar o pacote
25
+ com o objetivo de reconstruir, reimplementar ou derivar a sua lógica, os seus
26
+ contratos ou o seu conteúdo, salvo na medida em que a lei aplicável proíba essa
27
+ restrição.
28
+ 5. Remover, alterar ou ocultar avisos de autoria, de direitos ou esta licença.
29
+ 6. Sublicenciar, alugar, vender ou revender o pacote ou o acesso a ele.
30
+
31
+ ## Sem garantia
32
+
33
+ O SOFTWARE É FORNECIDO "COMO ESTÁ", SEM GARANTIA DE QUALQUER NATUREZA, EXPRESSA OU
34
+ IMPLÍCITA, INCLUINDO AS DE ADEQUAÇÃO A UM FIM ESPECÍFICO E DE NÃO VIOLAÇÃO. EM NENHUMA
35
+ HIPÓTESE O TITULAR RESPONDE POR QUALQUER DANO DECORRENTE DO USO DO SOFTWARE.
36
+
37
+ ## Rescisão
38
+
39
+ Os direitos acima terminam automaticamente com o descumprimento de qualquer item de "O
40
+ que você não pode fazer". Nesse caso, desinstale o pacote e as cópias que ele gravou.
41
+
42
+ ## Outros usos
43
+
44
+ Qualquer uso fora do permitido acima depende de autorização escrita do titular.
45
+
46
+ ---
47
+
48
+ Proprietary software, all rights reserved. Published for use, not for copying:
49
+ installing and running it is permitted; redistribution, derivative works, extracting
50
+ its content into another product or dataset, and reverse engineering are not. No
51
+ warranty. See the Portuguese text above for the governing terms.
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Skills com escopo explícito, conhecimento especializado preservado e fluxos comuns reutilizáveis.
4
4
 
5
- **Versão 0.12.2** ([mudanças](CHANGELOG.md)): o snapshot de cada sessão (`crow-skills events snapshot`) passa a trazer o pid do CLI de agente e o terminal dele (`pid`, `tty`), para o crow-term achar o painel de cada sessão. A 0.12.1 corrigiu a proteção de processos registrados e um teste instável; a 0.12.0 fez a crow-behaviour passar a encerrar a árvore inteira de todo processo que a execução inicia (com timeout, registro no ledger e verificação no fim) e `crow-skills resources --orphans` encontra e, com `--kill`, encerra só o que ficou órfão de ferramentas de agente. A 0.11.1 fez o Codex deixar de mostrar em dobro as skills que o Uniflow já projeta em `~/.codex/skills`. A 0.11.0 trouxe `crow-skills setup --yes`, que configura de uma vez todos os CLIs de agente da máquina, e `crow-skills upgrade --yes`, que atualiza tudo numa só passada; os hooks que o crow-skills grava já saem habilitados (no Codex, aprovados pelo próprio `codex app-server`, só os do crow-skills; `--no-trust` desliga) e `status`/`doctor` mostram se cada CLI vai rodá-los; a ajuda ficou curta e organizada por tarefa. A 0.10.1 passou a tratar swap e CPU como eixos de pressão no controle de recursos (swap quase cheio e carga alta reduzem agentes, jobs e workers; nenhuma recomendação cresce com a pressão), mostrou a CPU% dos CLIs de agente, trouxe `crow-skills resources --exec-prefix` para rodar jobs pesados em prioridade baixa e as regras da crow-behaviour sob pressão (reduzir já, um job pesado por vez, não repetir verificação, validação ampla no CI); os hooks do Codex vão para o `config.toml` quando o `hooks.json` é symlink. A 0.10.0 trouxe o crow-broadcast, que compartilha, de forma passiva, o que cada sessão e cada CLI (Claude Code, Codex, Gemini CLI, Antigravity, Devin, inclusive dentro do Uniflow) está fazendo nesta máquina: repositório, clone, branch, execução crow em andamento e arquivos em edição; o prompt recebe em toda mensagem um bloco com as sessões vivas que afetam este trabalho (ou uma linha dizendo que não há nenhuma), editar um arquivo que outra sessão tocou ou trocar de branch num clone em uso gera aviso (ou bloqueio, se configurado), e `crow-skills broadcast claim` reserva áreas; a crow-behaviour consulta isso antes de planejar e reserva as áreas que delega ([crow-broadcast](docs/broadcast.md)). A 0.9.0 trouxe o controle de memória da máquina: `crow-skills resources` mede memória, pressão e os CLIs de agente em execução e recomenda quantos agentes, jobs pesados e workers rodar em paralelo; a crow-behaviour consulta essa recomendação antes de cada fan-out (sob `warn` reduz, sob `critical` segue sequencial), limita workers de builds e testes, reutiliza um dev server por projeto e encerra o que abriu, e 15 especialistas que sobem processos pesados ganharam a regra correspondente; o hook de prompt e as status lines só avisam quando a máquina está sob pressão. A 0.8.0 trouxe `crow-skills mode on|off`, que liga e desliga a crow-behaviour em todas as sessões e CLIs (Claude Code, Codex, Gemini CLI, Antigravity, Devin) a partir do próximo prompt, com `/crow on|off` dentro da sessão e o indicador `▸▸ crow on` na status line; a crow-behaviour mostra o progresso como a lista de agentes do host (`● main`, `○ dono atividade`); `crow-skills events` publica um barramento de eventos dos hooks para terminais; `crow-skills autoupdate on` atualiza o CLI e as skills gerenciadas todo dia e no login. A 0.7.1 trouxe as correções da revalidação no Codex às skills especialistas; a 0.7.0 tornou o Uniflow agente de instalação (`--agents uniflow`); a 0.6.1 passou a mostrar o progresso da crow-behaviour na própria conversa; a 0.6.0 trouxe as regras de entrega das 32 skills especialistas, a partir da [validação no Codex](docs/evaluations/2026-10-26-codex.md).
5
+ **Versão 0.23.0**: o pacote passa a ser público no npm. A 0.22.0 deu conversa de verdade ao crow-broadcast (thread, resposta endereçada a quem perguntou, recibo de entrega e leitura, e entrega entre chamadas de ferramenta em vez de só no próximo prompt) e trouxe os micro-agents de trabalho braçal com contexto limpo. A 0.12.2 fez o snapshot de cada sessão (`crow-skills events snapshot`) passar a trazer o pid do CLI de agente e o terminal dele (`pid`, `tty`), para o crow-term achar o painel de cada sessão. A 0.12.1 corrigiu a proteção de processos registrados e um teste instável; a 0.12.0 fez a crow-behaviour passar a encerrar a árvore inteira de todo processo que a execução inicia (com timeout, registro no ledger e verificação no fim) e `crow-skills resources --orphans` encontra e, com `--kill`, encerra só o que ficou órfão de ferramentas de agente. A 0.11.1 fez o Codex deixar de mostrar em dobro as skills que o Uniflow já projeta em `~/.codex/skills`. A 0.11.0 trouxe `crow-skills setup --yes`, que configura de uma vez todos os CLIs de agente da máquina, e `crow-skills upgrade --yes`, que atualiza tudo numa só passada; os hooks que o crow-skills grava já saem habilitados (no Codex, aprovados pelo próprio `codex app-server`, só os do crow-skills; `--no-trust` desliga) e `status`/`doctor` mostram se cada CLI vai rodá-los; a ajuda ficou curta e organizada por tarefa. A 0.10.1 passou a tratar swap e CPU como eixos de pressão no controle de recursos (swap quase cheio e carga alta reduzem agentes, jobs e workers; nenhuma recomendação cresce com a pressão), mostrou a CPU% dos CLIs de agente, trouxe `crow-skills resources --exec-prefix` para rodar jobs pesados em prioridade baixa e as regras da crow-behaviour sob pressão (reduzir já, um job pesado por vez, não repetir verificação, validação ampla no CI); os hooks do Codex vão para o `config.toml` quando o `hooks.json` é symlink. A 0.10.0 trouxe o crow-broadcast, que compartilha, de forma passiva, o que cada sessão e cada CLI (Claude Code, Codex, Gemini CLI, Antigravity, Devin, inclusive dentro do Uniflow) está fazendo nesta máquina: repositório, clone, branch, execução crow em andamento e arquivos em edição; o prompt recebe em toda mensagem um bloco com as sessões vivas que afetam este trabalho (ou uma linha dizendo que não há nenhuma), editar um arquivo que outra sessão tocou ou trocar de branch num clone em uso gera aviso (ou bloqueio, se configurado), e `crow-skills broadcast claim` reserva áreas; a crow-behaviour consulta isso antes de planejar e reserva as áreas que delega (crow-broadcast). A 0.9.0 trouxe o controle de memória da máquina: `crow-skills resources` mede memória, pressão e os CLIs de agente em execução e recomenda quantos agentes, jobs pesados e workers rodar em paralelo; a crow-behaviour consulta essa recomendação antes de cada fan-out (sob `warn` reduz, sob `critical` segue sequencial), limita workers de builds e testes, reutiliza um dev server por projeto e encerra o que abriu, e 15 especialistas que sobem processos pesados ganharam a regra correspondente; o hook de prompt e as status lines só avisam quando a máquina está sob pressão. A 0.8.0 trouxe `crow-skills mode on|off`, que liga e desliga a crow-behaviour em todas as sessões e CLIs (Claude Code, Codex, Gemini CLI, Antigravity, Devin) a partir do próximo prompt, com `/crow on|off` dentro da sessão e o indicador `▸▸ crow on` na status line; a crow-behaviour mostra o progresso como a lista de agentes do host (`● main`, `○ dono atividade`); `crow-skills events` publica um barramento de eventos dos hooks para terminais; `crow-skills autoupdate on` atualiza o CLI e as skills gerenciadas todo dia e no login. A 0.7.1 trouxe as correções da revalidação no Codex às skills especialistas; a 0.7.0 tornou o Uniflow agente de instalação (`--agents uniflow`); a 0.6.1 passou a mostrar o progresso da crow-behaviour na própria conversa; a 0.6.0 trouxe as regras de entrega das 32 skills especialistas, a partir da validação no Codex.
6
6
 
7
- **Reconstrução em andamento.** A primeira versão com 32 contratos genéricos não atende à preservação de capacidades solicitada. Não use esse número nem o mapeamento por categoria como prova de substituição das originais. A revisão passa a abranger cada definição e seus recursos, modos e dependências, sem número fixo de skills ou limite artificial de linhas. Veja [critérios e progresso](docs/reconstruction.md).
7
+ **Reconstrução em andamento.** A primeira versão com 32 contratos genéricos não atende à preservação de capacidades solicitada. Não use esse número nem o mapeamento por categoria como prova de substituição das originais. A revisão passa a abranger cada definição e seus recursos, modos e dependências, sem número fixo de skills ou limite artificial de linhas. Veja critérios e progresso.
8
8
 
9
9
  ## Começar
10
10
 
@@ -14,7 +14,7 @@ Uma linha configura todos os CLIs de agente da máquina (Claude Code, Codex, Gem
14
14
  npm install -g @andespindola/crow-skills && crow-skills setup --yes
15
15
  ```
16
16
 
17
- O pacote é privado no npm: a instalação exige um `~/.npmrc` autenticado no escopo `@andespindola`. O tap do Homebrew foi aposentado na 0.16.1 e está congelado nessa versão — quem instalou por lá troca com `brew uninstall crow-skills` seguido da linha acima.
17
+ O pacote é público no npm desde a 0.23.0: instala sem autenticação e sem `~/.npmrc` no escopo. O código-fonte não é aberto — o repositório é privado e o pacote é distribuído sob os termos de `LICENSE`. O tap do Homebrew foi aposentado na 0.16.1 e está congelado nessa versão — quem instalou por lá troca com `brew uninstall crow-skills` seguido da linha acima.
18
18
 
19
19
  `setup` detecta cada CLI (binário no PATH ou diretório de configuração), instala todas as skills, o guia, as integrações e os hooks do modo crow já habilitados, liga o modo crow e o autoupdate, e termina com uma tabela por CLI e o doctor. `crow-skills setup --dry-run` mostra o plano sem gravar nada; rodar de novo só corrige o que mudou. Para atualizar tudo depois: `crow-skills upgrade --yes`. `crow-skills` sozinho mostra a ajuda curta e o estado atual; `crow-skills help <comando>` detalha cada comando. Detalhes em [setup, upgrade e ajuda](docs/cli.md#configurar-a-máquina-setup).
20
20
 
@@ -35,11 +35,11 @@ Exemplos:
35
35
  - Dados Postgres: `crow-postgres` + teste do efeito real; não precisa carregar toda a biblioteca.
36
36
  - Revisão: `crow-security-review` não modifica ou executa o alvo. Implementação é outra tarefa.
37
37
 
38
- [Catálogo e ownership](docs/catalog.md) · [Playbooks](docs/playbooks.md) · [Contrato de composição](docs/composition.md) · [Migração](docs/migration.md)
38
+ Catálogo e ownership · Playbooks · Contrato de composição
39
39
 
40
40
  ## Instalação e portabilidade
41
41
 
42
- O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm privada `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente. No [Uniflow](docs/cli.md#uniflow), `--agents uniflow --global` grava as skills no store dele (`~/.config/uniflow/skills`), sem tocar no resto da configuração. Em `--global`, instalar a crow-behaviour também grava o guia aditivo do agente (claude, codex, gemini, devin) para usar as skills; `--no-guide` desliga. As integrações de interface (status lines do Claude Code, do Codex e do Antigravity, hooks do modo crow em cada CLI, comando `/crow`, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga. O [barramento de eventos](docs/events.md) e a atualização automática (`crow-skills autoupdate on`) estão no [CLI](docs/cli.md).
42
+ O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm pública `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente. No [Uniflow](docs/cli.md#uniflow), `--agents uniflow --global` grava as skills no store dele (`~/.config/uniflow/skills`), sem tocar no resto da configuração. Em `--global`, instalar a crow-behaviour também grava o guia aditivo do agente (claude, codex, gemini, devin) para usar as skills; `--no-guide` desliga. As integrações de interface (status lines do Claude Code, do Codex e do Antigravity, hooks do modo crow em cada CLI, comando `/crow`, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga. O barramento de eventos e a atualização automática (`crow-skills autoupdate on`) estão no [CLI](docs/cli.md).
43
43
 
44
44
  ```sh
45
45
  npm install -g @andespindola/crow-skills@0.12.2
@@ -72,10 +72,10 @@ python3 -m unittest discover -s tests -v
72
72
 
73
73
  O checker verifica frontmatter no subconjunto YAML simples usado aqui, catálogo, owners únicos, referências locais autocontidas, handoffs válidos, placeholders e exposição de caminhos privados/credenciais. Não impõe limites artificiais ao corpo, não interpreta instruções das skills nem chama ferramentas externas.
74
74
 
75
- Os testes incluem mutações do pacote e invariantes de composição. Não medem a qualidade das respostas de um modelo. Avaliações comportamentais observadas, quando disponíveis, ficam em [docs/evaluations](docs/evaluations/README.md); não confundir texto esperado de fixture com teste executado por agente.
75
+ Os testes incluem mutações do pacote e invariantes de composição. Não medem a qualidade das respostas de um modelo. Avaliações comportamentais observadas, quando disponíveis, ficam no repositório, fora do pacote distribuído; não confundir texto esperado de fixture com teste executado por agente.
76
76
 
77
77
  ## Manutenção
78
78
 
79
79
  Fonte canônica: SKILL.md para instruções, catalog.json para papel/ownership/handoffs. O checker exige sincronismo de nome, descrição e saída. Altere o menor escopo que resolve a falha demonstrada. Veja [CONTRIBUTING.md](CONTRIBUTING.md).
80
80
 
81
- Sem alegação de ganho percentual ou auditoria integral da biblioteca anterior. [Proveniência e limites](docs/provenance.md).
81
+ Sem alegação de ganho percentual ou auditoria integral da biblioteca anterior. Proveniência e limites.
package/catalog.json CHANGED
@@ -36,7 +36,7 @@
36
36
  {
37
37
  "name": "crow-behaviour",
38
38
  "title": "Atuação coordenada",
39
- "description": "Use PROATIVAMENTE, sem o usuário pedir, em QUALQUER pedido que não seja conversa pura, pergunta factual curta ou ajuste trivial e local — inclusive quando é um arquivo só, uma decisão só ou só um plano: implementar uma feature, refatorar, depurar, auditar, revisar, migrar, planejar, pesquisar a fundo, tarefa multi-etapa, com exploração e decisões, ou que exija verificação antes de entregar. Carregue-a ANTES de começar; na dúvida, carregue. Nunca pergunte ao usuário se deve usá-la nem ofereça um critério mais estreito: com o modo crow ON a decisão já está tomada. Uma vez ativa, segue em uso nos prompts seguintes da sessão — mesmo plano, mesmo ledger e mesma validação por fases, retomados por conta própria quando faltarem. Ao ativar: decomponha o trabalho, delegue com as capacidades reais do runtime, verifique de forma adversarial e sintetize evidências. Não simule agentes nem altere permissões.",
39
+ "description": "Use PROATIVAMENTE, sem o usuário pedir, em QUALQUER pedido que não seja conversa pura, pergunta factual curta ou ajuste trivial e local — inclusive quando é um arquivo só, uma decisão só ou só um plano: implementar uma feature, refatorar, depurar, auditar, revisar, migrar, planejar, pesquisar a fundo, tarefa multi-etapa, com exploração e decisões, ou que exija verificação antes de entregar. Carregue-a ANTES de começar; na dúvida, carregue. Nunca pergunte ao usuário se deve usá-la nem ofereça critério mais estreito: com o modo crow ON a decisão já está tomada. Uma vez ativa, segue em uso nos prompts seguintes da sessão — mesmo plano, mesmo ledger e mesma validação por fases, retomados por conta própria quando faltarem. Ao ativar: decomponha, delegue o braçal a micro-agents de escopo mínimo com as capacidades reais do runtime, verifique de forma adversarial e sintetize evidências. Planejar, decidir, verificar e registrar são seus; o executor informa, você registra. Não simule agentes nem altere permissões.",
40
40
  "role": "coordinator",
41
41
  "owns": "execution-ledger",
42
42
  "next": []
@@ -1,3 +1,3 @@
1
1
  """Portable, local-first installer for the crow-skills catalogue."""
2
2
 
3
- __version__ = "0.22.0"
3
+ __version__ = "0.24.0"
@@ -48,6 +48,9 @@ COMMANDS = {
48
48
  "status": ("Instalações, guias, integrações, modo e autoupdate, por agente.", ["crow-skills status", "crow-skills status --agents claude"]),
49
49
  "doctor": ("Como status, com healthy true/false: skills editadas/ausentes, guia alterado, hooks não aprovados, transação pendente.",
50
50
  ["crow-skills doctor", "crow-skills doctor --json"]),
51
+ "skills": ("Versão de cada skill instalada e se está na revisão da fonte atual (latest) ou atrasada; somente leitura.",
52
+ ["crow-skills skills", "crow-skills skills --outdated", "crow-skills skills --json",
53
+ "crow-skills skills --agents claude --check-npm"]),
51
54
  "info": ("Detalhes de uma skill do catálogo.", ["crow-skills info crow-backend"]),
52
55
  "related": ("Handoffs, overlays e disponibilidade de complementos de uma skill.", ["crow-skills related crow-backend --agents claude --global"]),
53
56
  "install": ("Instala skills escolhidas em agentes e escopo explícitos, com preview, backup e transação.",
@@ -151,6 +154,10 @@ def parser():
151
154
  child.add_argument("topic", nargs="?", help="Comando, ou flags")
152
155
  for name in ("list", "groups", "agents", "status", "doctor"):
153
156
  command_parser(sub, name, [common])
157
+ child = command_parser(sub, "skills", [common])
158
+ child.add_argument("--outdated", action="store_true", help="Só as skills que não estão na revisão da fonte atual")
159
+ child.add_argument("--check-npm", dest="check_npm", action="store_true",
160
+ help="Também consultar o npm se o crow-skills instalado é a última versão publicada (única chamada de rede)")
154
161
  for name in ("info", "related"):
155
162
  child = command_parser(sub, name, [common])
156
163
  child.add_argument("name")
@@ -978,6 +985,160 @@ def group_coverage(rows):
978
985
  for v in by_target.values() if v["names"]]
979
986
 
980
987
 
988
+ # Estado de versão de uma skill instalada, contra a fonte deste crow-skills. "latest" é o único
989
+ # estado em dia; os demais ou pedem conserto (update/install) ou não são instalação gerenciada.
990
+ SKILL_STATE_WORDS = {
991
+ "latest": "igual à fonte atual",
992
+ "desatualizada": "revisão/origem diferente da fonte atual (crow-skills update)",
993
+ "modificada": "editada no disco",
994
+ "desativada": "instalada, mas desabilitada",
995
+ "ausente": "registrada e não encontrada no disco",
996
+ "nao_gerenciada": "não gerenciada por este CLI",
997
+ "indeterminada": "não foi possível ler o destino",
998
+ }
999
+ SKILL_PROBLEM_STATES = ("desatualizada", "modificada", "ausente")
1000
+
1001
+
1002
+ def skill_state(row, source):
1003
+ """Classifica UMA linha de store.status contra `source`. Regra única, usada por `skills`,
1004
+ `status` e `doctor` — a visibilidade e o veredito nunca podem discordar."""
1005
+ status = row.get("status")
1006
+ if status == "installed":
1007
+ if row.get("revision") == source.revision and row.get("source") == str(source.path):
1008
+ return "latest"
1009
+ return "desatualizada"
1010
+ if status == "modified":
1011
+ return "modificada"
1012
+ if status == "disabled":
1013
+ return "desativada"
1014
+ if status == "missing":
1015
+ return "ausente"
1016
+ if status in ("unmanaged", "absent", "external_link", "uniflow_link"):
1017
+ return "nao_gerenciada"
1018
+ return "indeterminada" # "unsafe" e qualquer estado novo: nunca contado como em dia
1019
+
1020
+
1021
+ def outdated_rows(rows, source):
1022
+ """Só as linhas instaladas atrasadas em relação à fonte: o que `status`/`doctor` precisam avisar."""
1023
+ return [r for r in rows if r.get("name") and skill_state(r, source) == "desatualizada"]
1024
+
1025
+
1026
+ def outdated_warning(rows, source):
1027
+ """Aviso skills_outdated (ou None). Mesmo texto em status e doctor."""
1028
+ outdated = outdated_rows(rows, source)
1029
+ if not outdated:
1030
+ return None
1031
+ names = sorted({r["name"] for r in outdated})
1032
+ agents = sorted({a for r in outdated for a in (r.get("agents") or [])})
1033
+ return {"code": "skills_outdated", "skills": names, "agents": agents,
1034
+ "message": str(len(names)) + " skill(s) instalada(s) estão atrasadas em relação à fonte atual ("
1035
+ + short_revision(source.revision) + " em " + str(source.path) + "): "
1036
+ + ", ".join(names) + ". Veja detalhes com crow-skills skills --outdated.",
1037
+ "fix": fix_command(agents)}
1038
+
1039
+
1040
+ def fix_command(agents):
1041
+ """O comando exato que conserta: o mesmo texto no rodapé de `skills` e no aviso de status/doctor."""
1042
+ return "crow-skills update --all --agents " + (",".join(agents) or ",".join(AGENTS)) + " --global --yes"
1043
+
1044
+
1045
+ def short_revision(revision):
1046
+ return (str(revision)[:12] if revision else "—")
1047
+
1048
+
1049
+ def npm_latest():
1050
+ """Última versão publicada do pacote npm, ou um motivo. Única chamada de rede do comando;
1051
+ sem npm ou sem rede devolve reason e NUNCA inventa um número."""
1052
+ import shutil
1053
+ import subprocess
1054
+ npm = shutil.which("npm")
1055
+ if not npm:
1056
+ return {"checked": False, "latest": None, "reason": "npm não encontrado no PATH"}
1057
+ try:
1058
+ done = subprocess.run([npm, "view", autoupdate.PACKAGE, "version"], capture_output=True, text=True,
1059
+ timeout=20, stdin=subprocess.DEVNULL, errors="replace")
1060
+ except (OSError, subprocess.SubprocessError):
1061
+ return {"checked": False, "latest": None, "reason": "a consulta ao npm falhou ou excedeu o tempo"}
1062
+ latest = (done.stdout or "").strip().splitlines()
1063
+ if done.returncode != 0 or not latest:
1064
+ return {"checked": False, "latest": None, "reason": "o npm não respondeu uma versão"}
1065
+ return {"checked": True, "latest": ui.clean(latest[-1].strip()), "reason": None}
1066
+
1067
+
1068
+ def skills_command(args, home, store):
1069
+ """Somente leitura: versão e estado de cada skill instalada, contra a fonte atual."""
1070
+ source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"))
1071
+ targets = scope(args, home, read=True)
1072
+ rows = store.status(targets)
1073
+ items = {}
1074
+ for row in rows:
1075
+ if not row.get("name"):
1076
+ continue
1077
+ state = skill_state(row, source)
1078
+ # Uma skill é UMA linha; agentes se agregam. Estados divergentes entre agentes viram
1079
+ # linhas separadas, porque um "latest" não pode esconder um "desatualizada" ao lado.
1080
+ item = items.setdefault((row["name"], state),
1081
+ {"name": row["name"], "state": state, "agents": [], "revisions": []})
1082
+ for agent in row.get("agents") or []:
1083
+ if agent not in item["agents"]:
1084
+ item["agents"].append(agent)
1085
+ if row.get("revision") and row["revision"] not in item["revisions"]:
1086
+ item["revisions"].append(row["revision"])
1087
+ skills = []
1088
+ for key in sorted(items):
1089
+ item = items[key]
1090
+ skills.append({"name": item["name"], "agents": sorted(item["agents"]), "state": item["state"],
1091
+ "meaning": SKILL_STATE_WORDS[item["state"]],
1092
+ "revision": item["revisions"][0] if len(item["revisions"]) == 1 else None,
1093
+ "revisions": sorted(item["revisions"])})
1094
+ shown = [s for s in skills if s["state"] != "latest"] if getattr(args, "outdated", False) else skills
1095
+ counts = {state: len([s for s in skills if s["state"] == state]) for state in SKILL_PROBLEM_STATES}
1096
+ affected = sorted({a for s in skills if s["state"] in SKILL_PROBLEM_STATES for a in s["agents"]})
1097
+ data = {"version": __version__, "source": str(source.path), "revision": source.revision,
1098
+ "skills": shown, "counts": counts, "outdated_only": bool(getattr(args, "outdated", False)),
1099
+ "fix": fix_command(affected) if any(counts.values()) else None}
1100
+ if getattr(args, "check_npm", False):
1101
+ data["npm"] = {**npm_latest(), "installed": __version__}
1102
+ if data["npm"]["checked"]:
1103
+ data["npm"]["up_to_date"] = data["npm"]["latest"] == __version__
1104
+ return data
1105
+
1106
+
1107
+ def render_skills(data, stream=None):
1108
+ stream = stream or sys.stdout
1109
+ lines = [ui.paint("crow-skills " + data["version"], "bold", stream) + " · fonte " + ui.clean(data["source"]) +
1110
+ " · revisão " + short_revision(data["revision"]), ""]
1111
+ if data["skills"]:
1112
+ table = [[s["name"], ",".join(s["agents"]),
1113
+ short_revision(s["revision"]) if s["revision"] else
1114
+ (",".join(short_revision(r) for r in s["revisions"]) or "—"),
1115
+ s["state"]] for s in data["skills"]]
1116
+ lines += ui.table(table, ["skill", "agentes", "revisão", "estado"], stream)
1117
+ elif data["outdated_only"]:
1118
+ lines.append(" " + ui.mark("ok", stream) + " Nenhuma skill fora da revisão da fonte atual.")
1119
+ else:
1120
+ lines.append(" Nenhuma skill instalada; comece com: crow-skills setup --yes")
1121
+ npm = data.get("npm")
1122
+ if npm:
1123
+ if not npm["checked"]:
1124
+ lines += ["", " npm: não foi possível conferir a última versão publicada (" + ui.clean(npm["reason"]) + ")."]
1125
+ elif npm.get("up_to_date"):
1126
+ lines += ["", " " + ui.mark("ok", stream) + " npm: " + npm["latest"] + " é a última publicada."]
1127
+ else:
1128
+ lines += ["", " " + ui.mark("warn", stream) + " npm: instalado " + npm["installed"] + ", publicado " +
1129
+ npm["latest"] + ".\n Para corrigir: crow-skills upgrade --yes"]
1130
+ counts = data["counts"]
1131
+ if any(counts.values()):
1132
+ lines += ["", ui.mark("warn", stream) + " " +
1133
+ ", ".join(str(counts[s]) + " " + s + ("s" if counts[s] != 1 else "")
1134
+ for s in SKILL_PROBLEM_STATES if counts[s]) +
1135
+ "\n Para corrigir: " + data["fix"]]
1136
+ if counts["modificada"] or counts["ausente"]:
1137
+ # O comando acima não toca o que foi editado/removido sem autorização explícita.
1138
+ lines.append(" Skill editada ou ausente só é substituída acrescentando --overwrite-modified.")
1139
+ return "\n".join(lines)
1140
+
1141
+
981
1142
  def groups_command(args):
982
1143
  source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"), getattr(args, "fetch", False))
983
1144
  available = groups_mod.load(source)
@@ -1046,6 +1207,8 @@ def run(args):
1046
1207
  return data
1047
1208
  if args.command == "autoupdate":
1048
1209
  return autoupdate_command(args, home, store, interactive)
1210
+ if args.command == "skills":
1211
+ return skills_command(args, home, store)
1049
1212
  if args.command in ("status", "doctor"):
1050
1213
  targets = scope(args, home, read=True)
1051
1214
  rows = unmanaged_projections(store, home, store.status(targets))
@@ -1086,6 +1249,13 @@ def run(args):
1086
1249
  "Codex continua carregando de lá.",
1087
1250
  "fix": "crow-skills setup --agents codex --yes (instala no diretório do Codex e migra), ou "
1088
1251
  "crow-skills uninstall " + ",".join(leftovers) + " --dest " + str(legacy_shared_dir(home)) + " --yes"})
1252
+ try:
1253
+ current = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"))
1254
+ except (CrowError, OSError):
1255
+ current = None # sem fonte legível não há com o que comparar; nenhum aviso inventado
1256
+ outdated = outdated_warning(rows, current) if current is not None else None
1257
+ if outdated:
1258
+ result.setdefault("warnings", []).append(outdated)
1089
1259
  result["groups"] = group_coverage(rows)
1090
1260
  try:
1091
1261
  result["autoupdate"] = autoupdate.status(store, home) if global_view else None
@@ -1095,7 +1265,10 @@ def run(args):
1095
1265
  result["hooks"] = [{"integration": r["integration"], "agents": r.get("agents"), "state": r["enabled"].get("state"),
1096
1266
  "fix": r["enabled"].get("fix")} for r in hooks]
1097
1267
  if args.command == "doctor":
1098
- result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides)
1268
+ # Uma skill instalada atrasada em relação à fonte não é "healthy": o doctor existe para
1269
+ # dizer que falta rodar o update, não para dar ✓ com pacote velho no disco.
1270
+ result.update(healthy=not pending and not outdated
1271
+ and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides)
1099
1272
  and (result["autoupdate"] or {}).get("healthy", True) is not False
1100
1273
  and all(h["state"] not in machine.BAD_HOOKS for h in result["hooks"]),
1101
1274
  agents=inventory(home))
@@ -1300,6 +1473,8 @@ def emit(command, data, as_json):
1300
1473
  print(machine.render_upgrade(data))
1301
1474
  elif command in ("status", "doctor"):
1302
1475
  print(render_status(data, doctor=command == "doctor"))
1476
+ elif command == "skills":
1477
+ print(render_skills(data))
1303
1478
  elif command == "progress":
1304
1479
  if data is not None:
1305
1480
  print(data["text"] or "crow: nenhuma execução registrada neste diretório.")
@@ -256,6 +256,9 @@ def render_body(agent, catalog, revision, skills):
256
256
  "- Qualquer pedido que não seja conversa pura, pergunta factual curta ou ajuste trivial e local — inclusive quando é um arquivo só, uma decisão só ou só um plano: carregue a skill crow-behaviour ANTES de começar, por iniciativa própria, sem esperar pedido. Para carregar, " + mech["load"] + "; por exemplo, " + invoke(name="crow-behaviour") + ". Siga-a até a entrega.",
257
257
  "- Vale também quando a entrega é só um plano, um diagnóstico ou uma revisão, ou quando não há código no diretório: depurar, auditar, investigar, migrar ou planejar uma mudança continua entrando. Na dúvida, carregue.",
258
258
  "- Uma vez ativa, ela segue em uso nos prompts seguintes da sessão: mesmo plano, mesmo ledger e mesma validação por fases, e cada novo pedido entra como unidade nova em vez de recomeçar sem registro. Se perceber que parou de mantê-los, retome na mesma resposta, registre a lacuna e siga — sem pedir permissão.",
259
+ "- Com ela ativa, o braçal é delegado por padrão: unidade cujo resultado você consegue especificar e aceitar pela evidência vai para um micro-agent de escopo mínimo e focado, com uma saída e nada \"enquanto estiver lá\". Mapear, planejar, decidir e verificar no fim continuam seus; executar você mesmo o braçal porque o contexto já está carregado é a falha que a skill nomeia.",
260
+ "- O micro-agent não recebe responsabilidade além de executar o que foi delegado: não grava memória nem notas, não mantém nada em sincronia, não mexe em git, não avisa outras sessões e não amplia o escopo — ele informa tudo a você no retorno (o que mudou, a saída real, o que não verificou, o que falhou, o que ficou pela metade).",
261
+ "- Salvar na memória durável e manter tudo em sincronia é seu e não se delega, e o registro acontece à medida que os retornos chegam, não num mutirão no fim: o retorno do executor é a única fonte do registro, e retorno não registrado é trabalho que ninguém sabe que foi feito.",
259
262
  "- Conversa, pergunta factual curta ou ajuste trivial e local: responda direto, sem carregá-la.",
260
263
  "",
261
264
  "## Skills especialistas",
@@ -344,6 +344,53 @@ GEMINI_EVENTS = ["SessionStart", "BeforeAgent", "BeforeTool", "AfterTool", "Afte
344
344
  AGY_EVENTS = ["SessionStart", "PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"]
345
345
  DEVIN_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PermissionRequest", "SubagentStop", "Stop",
346
346
  "SessionEnd"]
347
+ _DEVIN_ACCEPTED = {}
348
+
349
+
350
+ def devin_accepted():
351
+ """(eventos aceitos | None, motivo da falha): a sonda do devin, uma vez por binário no processo."""
352
+ binary = hooktrust.devin_binary()
353
+ # Pelo caminho RESOLVIDO: o devin se atualiza repontando ~/.local/bin/devin -> _versions/current,
354
+ # e uma imagem com mtimes normalizados (OCI, Nix) não mudaria a chave. Sem exists() antes do
355
+ # getmtime: entre os dois o symlink pode ser repontado, e aí a resposta é "não deu para perguntar".
356
+ try:
357
+ resolved = os.path.realpath(binary) if binary else None
358
+ key = (resolved, os.path.getmtime(resolved) if resolved else None)
359
+ except OSError as exc:
360
+ return None, "o binário do devin mudou enquanto era consultado (" + exc.__class__.__name__ + ")"
361
+ if key not in _DEVIN_ACCEPTED:
362
+ try:
363
+ _DEVIN_ACCEPTED[key] = (list(hooktrust.devin_hook_events(binary)), None)
364
+ except hooktrust.ProbeError as exc:
365
+ _DEVIN_ACCEPTED[key] = (None, str(exc))
366
+ return _DEVIN_ACCEPTED[key]
367
+
368
+
369
+ def devin_events():
370
+ """(events, note): the DEVIN_EVENTS this devin really accepts, plus what was dropped and why.
371
+
372
+ devin discards its ENTIRE hooks block when it rejects a single event, so an event this build
373
+ does not know would silently turn off every crow hook while the config still looked installed.
374
+ The accepted list is read from devin (trust.devin_hook_events); when devin cannot be asked, the
375
+ conservative baseline is written instead of guessing."""
376
+ accepted, failure = devin_accepted()
377
+ allowed = accepted if accepted is not None else list(hooktrust.DEVIN_BASELINE)
378
+ events = [e for e in DEVIN_EVENTS if e in allowed]
379
+ dropped = [e for e in DEVIN_EVENTS if e not in allowed]
380
+ if not events and accepted is not None:
381
+ # Nenhum evento em comum. O baseline NÃO serve de plano B aqui: seriam eventos que este
382
+ # devin recusa, e um evento recusado derruba o bloco inteiro. Melhor não gravar nada.
383
+ return [], ("Este devin não aceita nenhum dos eventos que o crow-skills usa (aceita: "
384
+ + ", ".join(accepted) + "); os hooks não foram gravados, porque um evento recusado "
385
+ "faz o devin descartar o bloco hooks inteiro.")
386
+ note = None
387
+ if dropped and accepted is not None:
388
+ note = ("Este devin não conhece " + ", ".join(dropped) + "; os eventos foram omitidos. Um evento desconhecido "
389
+ "faz o devin descartar o bloco hooks inteiro, então todos os hooks do crow-skills ficariam sem rodar.")
390
+ elif dropped:
391
+ note = ("Não foi possível perguntar ao devin quais eventos ele aceita (" + (failure or "motivo desconhecido")
392
+ + "); escritos só os eventos que todo devin conhecido aceita, sem " + ", ".join(dropped) + ".")
393
+ return events, note
347
394
  AGY_KEY = "crow-skills"
348
395
 
349
396
 
@@ -410,7 +457,7 @@ def hook_entries(store, home, python, host):
410
457
  if host == "agy":
411
458
  return {e: ([{"matcher": "", "hooks": [{"type": "command", "command": command(e), "timeout": 3}]}] if e in AGY_GROUPED
412
459
  else [{"type": "command", "command": command(e), "timeout": 3}]) for e in AGY_EVENTS}
413
- return {e: [{"hooks": [{"type": "command", "command": command(e)}]}] for e in DEVIN_EVENTS}
460
+ return {e: [{"hooks": [{"type": "command", "command": command(e)}]}] for e in devin_events()[0]}
414
461
 
415
462
 
416
463
  def is_ours(entry, store):
@@ -540,6 +587,11 @@ def hooks_sync(name, store, home, record, desired, python, dry):
540
587
  return None, "remove", None
541
588
  if not quoted_ok(python, hook_script_path(store), home):
542
589
  return record, "skip", "Caminho do Python, do estado ou do home contém aspas/variáveis; hooks não configurados."
590
+ if spec["host"] == "devin":
591
+ events, devin_note = devin_events()
592
+ if not events:
593
+ # Gravar um bloco vazio diria "instalado" sem hook nenhum, e a nota se perderia.
594
+ return (record if desired else None), "skip", devin_note
543
595
  entries = hook_entries(store, home, python, spec["host"])
544
596
  wanted = refresh_in_place(data, entries, store, spec["layout"]) or add_hooks(stripped, entries, spec["layout"])
545
597
  if wanted == data:
@@ -552,6 +604,8 @@ def hooks_sync(name, store, home, record, desired, python, dry):
552
604
  note = None
553
605
  if spec["host"] == "codex":
554
606
  note = (codex_approval_note() + codex_hooks_feature(home)).strip() or None
607
+ elif spec["host"] == "devin":
608
+ note = devin_events()[1]
555
609
  if dry:
556
610
  return new_record, action, note
557
611
  tracked_write(store, path, dump(wanted), old, spec["backup"])
@@ -1278,9 +1332,59 @@ def sync(store, home, wanted, dry_run=False, python=None, trust=True):
1278
1332
  return report
1279
1333
 
1280
1334
 
1335
+ def devin_hooks_state(home, probe):
1336
+ """Whether devin will run the hooks written in its config: {state, fix?}.
1337
+
1338
+ One event devin does not know makes it discard the whole hooks block, so a config that looks
1339
+ installed can run nothing. Reported instead of a green tick; the events in the file are
1340
+ compared with the list devin itself gives."""
1341
+ base = hooktrust.non_codex_state("devin", home)
1342
+ path = devin_dir(home) / "config.json"
1343
+ try:
1344
+ _, data, problem = read_json_object(protect(path, home), "~/.config/devin/config.json")
1345
+ except CrowError as exc:
1346
+ # Arquivo irregular (symlink, hard link, ilegível): antes isto estourava daqui e o chamador
1347
+ # engolia a exceção, derrubando o relatório de integrações INTEIRO e ainda imprimindo healthy.
1348
+ return dict(base, state="unknown", fix="~/.config/devin/config.json não pôde ser lido (" + str(exc) + ").")
1349
+ if problem:
1350
+ return dict(base, state="unknown", fix=problem)
1351
+ if "hooks" not in data:
1352
+ return base
1353
+ block = data.get("hooks")
1354
+ if not isinstance(block, dict):
1355
+ # O devin exige um mapa: "invalid type: sequence, expected a map" e descarta o bloco todo.
1356
+ return {"state": "feature_off",
1357
+ "fix": ("A chave hooks de ~/.config/devin/config.json não é um objeto JSON; o devin descarta o bloco "
1358
+ "inteiro e nenhum hook roda. Corrija o arquivo e rode crow-skills integrations install.")}
1359
+ if not block:
1360
+ return base
1361
+ written = list(block)
1362
+ if not probe:
1363
+ # Sem sonda dá para responder offline o caso comum: tudo o que está escrito é do conjunto
1364
+ # que todo devin conhecido aceita. Só o que está fora disso precisa perguntar ao devin.
1365
+ outside = [e for e in written if e not in hooktrust.DEVIN_BASELINE]
1366
+ if not outside:
1367
+ return base
1368
+ return dict(base, state="unknown",
1369
+ fix=("Evento(s) fora do conjunto que todo devin aceita (" + ", ".join(outside)
1370
+ + "); rode crow-skills doctor para conferir com o devin instalado."))
1371
+ accepted, failure = devin_accepted() # mesma sonda em cache: instalar e relatar não paga duas vezes
1372
+ if accepted is None:
1373
+ return dict(base, state="unknown", fix="Não foi possível perguntar ao devin quais eventos ele aceita (" + str(failure) + ").")
1374
+ foreign = [e for e in written if e not in accepted]
1375
+ if foreign:
1376
+ return {"state": "feature_off",
1377
+ "fix": ("~/.config/devin/config.json tem evento(s) que este devin não conhece (" + ", ".join(foreign)
1378
+ + "); ele descarta o bloco hooks INTEIRO, então nenhum hook roda. Rode crow-skills integrations install "
1379
+ "para regravar só os eventos aceitos (ou remova os eventos que não são do crow-skills).")}
1380
+ return base
1381
+
1382
+
1281
1383
  def hooks_enabled(name, store, home, record, probe=True):
1282
1384
  """Whether the CLI will actually run the crow hooks: {state, fix?} (see trust.py)."""
1283
1385
  host = HOOKS[name]["host"]
1386
+ if host == "devin":
1387
+ return devin_hooks_state(home, probe)
1284
1388
  if host != "codex":
1285
1389
  return hooktrust.non_codex_state(host, home)
1286
1390
  base = {}
@@ -638,5 +638,9 @@ class Store:
638
638
  status = "unsafe"
639
639
  rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
640
640
  "status": status, "revision": record.get("revision") if record else None,
641
+ # Origem e digest do pacote registrado: é o que permite dizer "atrasada em
642
+ # relação a esta fonte" sem reabrir o pacote (a mesma regra de `plan`).
643
+ "package_sha256": record.get("package_sha256") if record else None,
644
+ "source": record.get("source") if record else None,
641
645
  "activation_verified": False})
642
646
  return rows
@@ -24,10 +24,12 @@ reported from their own feature flags.
24
24
  """
25
25
  import json
26
26
  import os
27
+ import pathlib
27
28
  import queue
28
29
  import re
29
30
  import shutil
30
31
  import subprocess
32
+ import tempfile
31
33
  import threading
32
34
  import time
33
35
 
@@ -50,6 +52,77 @@ def codex_binary():
50
52
  return shutil.which("codex")
51
53
 
52
54
 
55
+ # The hook events every devin build known to the project accepts (measured on 3000.11.3, the
56
+ # oldest installed here). Used only when devin cannot be asked; never a claim about the build
57
+ # in front of us, which is what devin_hook_events() goes and reads.
58
+ DEVIN_BASELINE = ("PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "PostCompaction",
59
+ "SessionStart", "SessionEnd", "PermissionRequest")
60
+ DEVIN_PROBE_EVENT = "CrowSkillsCapabilityProbe"
61
+ # Measured: `devin doctor` answers in 0.56 s. This sits on the install path, so the ceiling is
62
+ # short on purpose — a wedged devin costs a conservative fallback, not a stalled install.
63
+ DEVIN_PROBE_TIMEOUT = 8.0
64
+ # Anchored on THREE things, because devin words every rejected enum the same way and emits the
65
+ # others first (theme_mode, ProxyMode, UnicodeMode): the key must be "hooks", the refused variant
66
+ # must be our probe event, and the list must end at devin's own ". Using the default", so a line
67
+ # that was wrapped, truncated or colorized does not match at all. Not matching is a ProbeError and
68
+ # a conservative fallback; a PARTIAL match would be a short list written as if complete, which is
69
+ # the silent-disable this whole probe exists to prevent.
70
+ _DEVIN_EXPECTED = re.compile(
71
+ r'invalid value for "hooks" in [^\n]*?: unknown variant `(?P<refused>[^`\n]*)`, '
72
+ r'expected one of (?P<accepted>`[^`\n]+`(?:, `[^`\n]+`)*)\. Using the default')
73
+
74
+
75
+ def devin_binary():
76
+ """The devin executable, or None. CROW_SKILLS_DEVIN overrides PATH (empty: treat as absent)."""
77
+ override = os.environ.get("CROW_SKILLS_DEVIN")
78
+ if override is not None:
79
+ return override if override and os.path.isfile(override) and os.access(override, os.X_OK) else None
80
+ return shutil.which("devin")
81
+
82
+
83
+ def devin_hook_events(binary=None, timeout=None):
84
+ """The hook events the installed devin accepts, asked of devin itself.
85
+
86
+ devin validates config.json's `hooks` against its own enum and, rejecting one event, names
87
+ every event it knows: "unknown variant `X`, expected one of `A`, `B`, ...". It also drops the
88
+ WHOLE hooks block when it rejects a single event ("Using the default ({})"), so writing an
89
+ event this build does not know silently disables every crow hook. One deliberately invalid
90
+ event in a throwaway config directory turns that refusal into a capability list; the probe
91
+ pins XDG_CONFIG_HOME at the throwaway, so the user's ~/.config/devin/config.json is never
92
+ read and never written.
93
+
94
+ Raises ProbeError with a short reason (never devin's whole output)."""
95
+ binary = binary or devin_binary()
96
+ timeout = DEVIN_PROBE_TIMEOUT if timeout is None else timeout
97
+ if not binary:
98
+ raise ProbeError("devin não encontrado no PATH")
99
+ with tempfile.TemporaryDirectory(prefix="crow-devin-probe-") as temp:
100
+ config = pathlib.Path(temp) / "devin"
101
+ config.mkdir()
102
+ (config / "config.json").write_text(json.dumps(
103
+ {"version": 1, "hooks": {DEVIN_PROBE_EVENT: [{"hooks": [{"type": "command", "command": "true"}]}]}}),
104
+ encoding="utf-8")
105
+ env = dict(os.environ, XDG_CONFIG_HOME=temp)
106
+ try:
107
+ done = subprocess.run([binary, "doctor"], cwd=temp, env=env, timeout=timeout,
108
+ stdin=subprocess.DEVNULL, capture_output=True, text=True,
109
+ encoding="utf-8", errors="replace")
110
+ except subprocess.TimeoutExpired:
111
+ raise ProbeError("devin doctor não respondeu em " + str(int(timeout)) + " s")
112
+ except OSError as exc:
113
+ raise ProbeError("devin doctor não iniciou (" + exc.__class__.__name__ + ")")
114
+ found = _DEVIN_EXPECTED.search((done.stderr or "") + "\n" + (done.stdout or ""))
115
+ if not found:
116
+ raise ProbeError("devin não listou os eventos aceitos de hooks")
117
+ if found.group("refused") != DEVIN_PROBE_EVENT:
118
+ # Outro evento foi recusado: a lista pode não ser a resposta à nossa sonda.
119
+ raise ProbeError("devin recusou " + (found.group("refused") or "um evento") + ", não a sonda")
120
+ events = re.findall(r"`([^`]+)`", found.group("accepted"))
121
+ if not events:
122
+ raise ProbeError("lista de eventos do devin vazia")
123
+ return events
124
+
125
+
53
126
  def camel(event):
54
127
  return event[:1].lower() + event[1:]
55
128
 
@@ -30,6 +30,6 @@ Esta tabela define ameaça/controle, não é um certificado de segurança. Resul
30
30
 
31
31
  ## Snapshot npm
32
32
 
33
- O launcher usa spawn sem shell e Python isolado (`-I`), evitando import do cwd/PYTHONPATH. Não há dependências npm nem hooks de instalação. Prepack verifica bytes commitados e rejeita arquivos extras nas globs de publicação; caches Python são explicitamente excluídos. O smoke valida o tarball inteiro contra allowlist/hashes antes de instalar em perfil isolado. Checksums não provam autoria: distribuição depende da integridade/autorização do registro npm. Publicação deve ser escopada e restricted, sem fallback público.
33
+ O launcher usa spawn sem shell e Python isolado (`-I`), evitando import do cwd/PYTHONPATH. Não há dependências npm nem hooks de instalação. Prepack verifica bytes commitados e rejeita arquivos extras nas globs de publicação; caches Python são explicitamente excluídos. O smoke valida o tarball inteiro contra allowlist/hashes antes de instalar em perfil isolado. Checksums não provam autoria: distribuição depende da integridade/autorização do registro npm. Publicação deve ser escopada e pública (0.23.0), sem volta silenciosa para restricted: o prepack e o smoke reprovam. O smoke também varre o payload publicado em busca de caminho para ferramenta externa de engenharia reversa (invocação, verificação de presença no PATH, instalação por gerenciador de pacotes ou identificador de evidência dela) e reprova a publicação se encontrar, inclusive em documentação. Varredura não prova ausência de toda referência possível.
34
34
 
35
35
  Resultados, falhas corrigidas e limites: [avaliação do CLI](evaluations/2026-10-20-cli.md).