@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 +51 -0
- package/README.md +7 -7
- package/catalog.json +1 -1
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/cli.py +176 -1
- package/crow_skills/guide.py +3 -0
- package/crow_skills/integrations.py +105 -1
- package/crow_skills/storage.py +4 -0
- package/crow_skills/trust.py +73 -0
- package/docs/cli-security.md +1 -1
- package/docs/cli.md +63 -3
- package/package.json +4 -3
- package/release.json +23 -18
- package/skills/crow-behaviour/SKILL.md +14 -7
- package/skills/crow-behaviour/references/micro-agents.md +52 -5
- package/skills/crow-behaviour/scripts/crow_micro.py +45 -0
- package/skills/crow-behaviour/scripts/micro_agent_instructions.md +15 -3
- package/skills/crow-browser/SKILL.md +39 -0
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.
|
|
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
|
|
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 é
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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": []
|
package/crow_skills/__init__.py
CHANGED
package/crow_skills/cli.py
CHANGED
|
@@ -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
|
-
|
|
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.")
|
package/crow_skills/guide.py
CHANGED
|
@@ -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
|
|
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 = {}
|
package/crow_skills/storage.py
CHANGED
|
@@ -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
|
package/crow_skills/trust.py
CHANGED
|
@@ -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
|
|
package/docs/cli-security.md
CHANGED
|
@@ -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
|
|
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).
|