@andespindola/crow-skills 0.27.1 → 0.29.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/README.md +4 -4
- package/catalog.json +26 -0
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/agents.py +35 -70
- package/crow_skills/autoupdate.py +4 -96
- package/crow_skills/cli.py +12 -157
- package/crow_skills/guide.py +33 -65
- package/crow_skills/machine.py +32 -194
- package/crow_skills/projection.py +3 -148
- package/crow_skills/storage.py +54 -86
- package/crow_skills/ui.py +3 -3
- package/docs/cli-security.md +1 -1
- package/docs/cli.md +16 -60
- package/package.json +1 -1
- package/release.json +30 -20
- package/skills/crow-behaviour/SKILL.md +4 -2
- package/skills/crow-behaviour/references/broadcast.md +1 -1
- package/skills/crow-behaviour/references/micro-agents.md +12 -5
- package/skills/crow-behaviour/references/orchestration.md +1 -1
- package/skills/crow-ml/SKILL.md +35 -0
- package/skills/crow-model-level/SKILL.md +35 -0
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Skills com escopo explícito, conhecimento especializado preservado e fluxos comuns reutilizáveis.
|
|
4
4
|
|
|
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.
|
|
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.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) 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.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
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
|
|
|
11
|
-
Uma linha configura
|
|
11
|
+
Uma linha configura os quatro perfis nativos independentes (Claude Code, Codex, Gemini CLI e Devin), as integrações do Antigravity e o segmento do tmux disponíveis na máquina:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
14
|
npm install -g @andespindola/crow-skills && crow-skills setup --yes
|
|
@@ -26,7 +26,7 @@ O pacote é público no npm desde a 0.23.0: instala sem autenticação e sem `~/
|
|
|
26
26
|
4. Entregue o artefato com alvo/revisão, evidência e pendências. Desconhecido não significa aprovado.
|
|
27
27
|
5. Carregue outra skill só quando houver decisão distinta a tomar. Handoffs são opcionais, não dependências ocultas.
|
|
28
28
|
|
|
29
|
-
Para adotar uma atuação mais coordenada em qualquer domínio, use `crow-behaviour`: todo pedido que não seja conversa, pergunta factual curta ou ajuste trivial e local recebe plano, delegação suportada, verificação e síntese, mantidos nos prompts seguintes da sessão; só essas exceções continuam diretas. A skill adapta o comportamento às ferramentas reais, inclusive execução sequencial, sem depender do Workflow do Claude. Fora dessas exceções ela mantém um ledger de progresso (`.crow/runs/`) e mostra na conversa o bloco de processos no formato da lista de agentes do host (`● main …` e `○ dono atividade` por unidade em andamento); `crow-skills progress --watch` e as status lines mostram o mesmo. `crow-skills mode on|off` (ou `/crow on|off`) a liga ou desliga em todos os CLIs. Com várias sessões ou CLIs nos mesmos repositórios, ela lê o bloco do crow-broadcast que chega em todo prompt (`crow-skills broadcast` detalha) para não duplicar trabalho nem trocar a branch de um clone em uso, e reserva as áreas que delega. Ela não
|
|
29
|
+
Para adotar uma atuação mais coordenada em qualquer domínio, use `crow-behaviour`: todo pedido que não seja conversa, pergunta factual curta ou ajuste trivial e local recebe plano, delegação suportada, verificação e síntese, mantidos nos prompts seguintes da sessão; só essas exceções continuam diretas. A skill adapta o comportamento às ferramentas reais, inclusive execução sequencial, sem depender do Workflow do Claude. Fora dessas exceções ela mantém um ledger de progresso (`.crow/runs/`) e mostra na conversa o bloco de processos no formato da lista de agentes do host (`● main …` e `○ dono atividade` por unidade em andamento); `crow-skills progress --watch` e as status lines mostram o mesmo. `crow-skills mode on|off` (ou `/crow on|off`) a liga ou desliga em todos os CLIs. Com várias sessões ou CLIs nos mesmos repositórios, ela lê o bloco do crow-broadcast que chega em todo prompt (`crow-skills broadcast` detalha) para não duplicar trabalho nem trocar a branch de um clone em uso, e reserva as áreas que delega. Ela não escolhe sozinha o modelo/effort: com `crow-model-level` instalada, esse nível é declarado por unidade delegada. [Escopo e evidência](analysis/crow-behaviour.md).
|
|
30
30
|
|
|
31
31
|
Exemplos:
|
|
32
32
|
- Plataforma nova: `crow-platform` coordena; `crow-discovery` e `crow-spec` definem o que falta; as especialistas implementam/verificam por fatia.
|
|
@@ -39,7 +39,7 @@ 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 pública `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente.
|
|
42
|
+
O pacote de cada skill é somente sua pasta em `skills/`. Cada perfil nativo recebe sua própria cópia; não há store de outro host, projeção ou descoberta de configuração de hosts removidos. 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. 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
|
package/catalog.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"description": "Coordenação e o que serve a quase todo projeto: arquitetura, especificação, depuração, testes e revisão de segurança",
|
|
20
20
|
"skills": [
|
|
21
21
|
"crow-behaviour",
|
|
22
|
+
"crow-model-level",
|
|
22
23
|
"crow-architecture",
|
|
23
24
|
"crow-spec",
|
|
24
25
|
"crow-debug",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"crow-backend",
|
|
48
49
|
"crow-api-test",
|
|
49
50
|
"crow-postgres",
|
|
51
|
+
"crow-ml",
|
|
50
52
|
"crow-domain",
|
|
51
53
|
"crow-architecture"
|
|
52
54
|
]
|
|
@@ -143,6 +145,16 @@
|
|
|
143
145
|
"owns": "execution-ledger",
|
|
144
146
|
"next": []
|
|
145
147
|
},
|
|
148
|
+
{
|
|
149
|
+
"name": "crow-model-level",
|
|
150
|
+
"title": "Nível de modelo por unidade",
|
|
151
|
+
"description": "Use quando houver unidade para delegar e for preciso dizer em que nível ela roda — qual modelo e qual esforço de raciocínio acompanham o micro-agent. Entra antes de cada delegação, mesmo quando é uma só, e de novo quando uma delegação anterior voltou cara demais ou fraca demais para o que a unidade exigia; é ela que satisfaz a exigência dos hosts de uma instrução explícita para que o esforço seja definido em vez de herdado. Não entra onde não há delegação: pedido que você mesmo resolve não tem unidade a nivelar. Se delega, como quebrar o pedido em unidades, o conteúdo do brief e quais skills vão nele, o teto de concorrência e o registro do que voltou são de crow-behaviour.",
|
|
152
|
+
"role": "specialist",
|
|
153
|
+
"owns": "unit-level-plan",
|
|
154
|
+
"next": [
|
|
155
|
+
"crow-behaviour"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
146
158
|
{
|
|
147
159
|
"name": "crow-platform",
|
|
148
160
|
"title": "Entrega de plataforma",
|
|
@@ -251,6 +263,20 @@
|
|
|
251
263
|
"crow-release"
|
|
252
264
|
]
|
|
253
265
|
},
|
|
266
|
+
{
|
|
267
|
+
"name": "crow-ml",
|
|
268
|
+
"title": "Modelo treinado e avaliado",
|
|
269
|
+
"description": "Use quando o pedido é treinar, avaliar ou retreinar um modelo, ou decidir se um modelo já medido pode entrar numa decisão real. Entra quando o artefato é a rodada de treino — dado, peso e métrica — e a pergunta é se o número obtido se sustenta fora dela. A extração e o desenho das colunas no banco são de crow-postgres; o serviço que serve o modelo e a regra de negócio em volta são de crow-backend; levar o peso para um ambiente é de crow-release; o alerta de deriva e o runbook de quem atende são de crow-observe; latência ou throughput medidos da inferência são de crow-performance. Base legal de dado pessoal e licença de corpus não têm dono nesta suíte: são decisão de quem responde pelo dado.",
|
|
270
|
+
"role": "specialist",
|
|
271
|
+
"owns": "ml-run-evidence",
|
|
272
|
+
"next": [
|
|
273
|
+
"crow-postgres",
|
|
274
|
+
"crow-backend",
|
|
275
|
+
"crow-release",
|
|
276
|
+
"crow-observe",
|
|
277
|
+
"crow-performance"
|
|
278
|
+
]
|
|
279
|
+
},
|
|
254
280
|
{
|
|
255
281
|
"name": "crow-threat-model",
|
|
256
282
|
"title": "Ameaças do desenho",
|
package/crow_skills/__init__.py
CHANGED
package/crow_skills/agents.py
CHANGED
|
@@ -15,9 +15,6 @@ AGENTS = {
|
|
|
15
15
|
"claude": (".claude/skills", ".claude/skills", "claude"),
|
|
16
16
|
"gemini": (".gemini/skills", ".gemini/skills", "gemini"),
|
|
17
17
|
"devin": (".config/devin/skills", ".devin/skills", "devin"),
|
|
18
|
-
# Uniflow keeps one store and links it into each CLI's user skills dir; it has
|
|
19
|
-
# no project scope. The store sits beside config.json (see uniflow_store).
|
|
20
|
-
"uniflow": (".config/uniflow/skills", None, "uniflow"),
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
|
|
@@ -45,6 +42,12 @@ def legacy_shared_dir(home):
|
|
|
45
42
|
return Path(physical(Path(home) / LEGACY_SHARED))
|
|
46
43
|
|
|
47
44
|
|
|
45
|
+
def reject_retired(path):
|
|
46
|
+
"""Refusal-only tombstone; never inspect retired data or consult its environment."""
|
|
47
|
+
if {part.lower() for part in Path(path).parts} & {"uniflow", "crowflow"}:
|
|
48
|
+
raise CrowError("unsupported_destination", "Namespace de host removido; nenhum conteúdo foi inspecionado.")
|
|
49
|
+
|
|
50
|
+
|
|
48
51
|
def physical(path):
|
|
49
52
|
"""The physical path behind a skills directory, following symlinks on the components that exist.
|
|
50
53
|
|
|
@@ -52,10 +55,34 @@ def physical(path):
|
|
|
52
55
|
itself follows the link. storage.checked() refuses to WRITE through a symlink, which is the
|
|
53
56
|
control that keeps a write from landing somewhere other than where it reads: resolving here, at
|
|
54
57
|
destination resolution, honours the dotfiles layout while leaving that control intact, because
|
|
55
|
-
what reaches checked() is already physical.
|
|
56
|
-
|
|
58
|
+
what reaches checked() is already physical. Component-wise link resolution rejects retired
|
|
59
|
+
namespaces before inspecting their target and leaves missing components lexical.
|
|
57
60
|
"""
|
|
58
|
-
|
|
61
|
+
current = Path(path).expanduser()
|
|
62
|
+
if not current.is_absolute():
|
|
63
|
+
current = Path.cwd() / current
|
|
64
|
+
# Reject retired namespaces lexically BEFORE touching any component in them.
|
|
65
|
+
reject_retired(current)
|
|
66
|
+
pending = list(current.parts[1:]); resolved = Path(current.anchor); hops = 0
|
|
67
|
+
while pending:
|
|
68
|
+
component = pending.pop(0)
|
|
69
|
+
if component == "..":
|
|
70
|
+
resolved = resolved.parent
|
|
71
|
+
continue
|
|
72
|
+
candidate = resolved / component
|
|
73
|
+
reject_retired(candidate)
|
|
74
|
+
if candidate.is_symlink():
|
|
75
|
+
hops += 1
|
|
76
|
+
if hops > 40:
|
|
77
|
+
raise CrowError("unsafe_destination", "Ciclo de symlinks no destino.")
|
|
78
|
+
link = Path(os.readlink(candidate))
|
|
79
|
+
target = link if link.is_absolute() else candidate.parent / link
|
|
80
|
+
reject_retired(target)
|
|
81
|
+
pending = list(target.parts[1:]) + pending
|
|
82
|
+
resolved = Path(target.anchor)
|
|
83
|
+
else:
|
|
84
|
+
resolved = candidate
|
|
85
|
+
return str(resolved)
|
|
59
86
|
|
|
60
87
|
|
|
61
88
|
def real_home(home):
|
|
@@ -63,38 +90,6 @@ def real_home(home):
|
|
|
63
90
|
return home == Path.home().resolve()
|
|
64
91
|
|
|
65
92
|
|
|
66
|
-
def uniflow_config_file(home):
|
|
67
|
-
"""Same resolution as Uniflow: UNIFLOW_CONFIG/CROWFLOW_CONFIG name config.json,
|
|
68
|
-
otherwise $XDG_CONFIG_HOME/uniflow/config.json, otherwise ~/.config/uniflow/config.json."""
|
|
69
|
-
if real_home(home):
|
|
70
|
-
override = os.getenv("UNIFLOW_CONFIG") or os.getenv("CROWFLOW_CONFIG")
|
|
71
|
-
if override:
|
|
72
|
-
return Path(os.path.abspath(Path(override).expanduser()))
|
|
73
|
-
if os.getenv("XDG_CONFIG_HOME"):
|
|
74
|
-
return Path(os.path.abspath(Path(os.environ["XDG_CONFIG_HOME"]).expanduser() / "uniflow/config.json"))
|
|
75
|
-
return Path(os.path.abspath(home / ".config/uniflow/config.json"))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def uniflow_config_dir(home):
|
|
79
|
-
return uniflow_config_file(home).parent
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def uniflow_guards(home):
|
|
83
|
-
"""Uniflow's own paths to protect: its whole config dir, unless that dir is the home
|
|
84
|
-
or one of its ancestors (UNIFLOW_CONFIG=~/uniflow.json). Then only Uniflow's files
|
|
85
|
-
and store are protected, never the home itself."""
|
|
86
|
-
config = uniflow_config_dir(home)
|
|
87
|
-
profile = Path(os.path.abspath(home))
|
|
88
|
-
if profile == config or profile.is_relative_to(config):
|
|
89
|
-
return [uniflow_config_file(home), config / "skills", config / ".bundled-skills.json",
|
|
90
|
-
config / "skills.required.json"]
|
|
91
|
-
return [config]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def uniflow_store(home):
|
|
95
|
-
return uniflow_config_dir(home) / "skills"
|
|
96
|
-
|
|
97
|
-
|
|
98
93
|
def destinations(names, home, project=None, custom=None):
|
|
99
94
|
if custom:
|
|
100
95
|
if names:
|
|
@@ -109,9 +104,6 @@ def destinations(names, home, project=None, custom=None):
|
|
|
109
104
|
if name not in AGENTS:
|
|
110
105
|
raise CrowError("unknown_agent", "Agente desconhecido: " + name)
|
|
111
106
|
user_path, project_path, _ = AGENTS[name]
|
|
112
|
-
if project and project_path is None:
|
|
113
|
-
raise CrowError("unsupported_scope", "O Uniflow só tem o store global (" + user_path + ", ao lado do config.json); "
|
|
114
|
-
"use --global. Ele projeta esse store nos agentes por conta própria.")
|
|
115
107
|
path = (Path(project) / project_path) if project else (home / user_path)
|
|
116
108
|
# Respect explicit XDG config, but isolate it when --home selects another profile.
|
|
117
109
|
if name == "devin" and not project and real_home(home) and os.getenv("XDG_CONFIG_HOME"):
|
|
@@ -120,8 +112,6 @@ def destinations(names, home, project=None, custom=None):
|
|
|
120
112
|
# install would write beside a profile Codex never reads.
|
|
121
113
|
if name == "codex" and not project:
|
|
122
114
|
path = codex_skills_dir(home)
|
|
123
|
-
if name == "uniflow":
|
|
124
|
-
path = uniflow_store(home)
|
|
125
115
|
key = physical(path)
|
|
126
116
|
result.setdefault(key, {"path": key, "agents": []})
|
|
127
117
|
if name not in result[key]["agents"]:
|
|
@@ -168,17 +158,13 @@ def refuse_implausible_destination(targets, home):
|
|
|
168
158
|
embaixo do diretório daquele CLI: um ~/.claude/skills apontando para ~/.ssh, para o .git de
|
|
169
159
|
outro repositório ou para o próprio diretório de transações do crow-skills passava a receber
|
|
170
160
|
pacotes. Regra POSITIVA, por decisão do dono: o caminho resolvido precisa terminar em "skills"
|
|
171
|
-
(o layout de dotfiles normal, /dotfiles/claude/skills, passa)
|
|
172
|
-
agente certo. Uma lista de negação seria mais fácil de furar com um caminho não previsto.
|
|
161
|
+
(o layout de dotfiles normal, /dotfiles/claude/skills, passa). Uma lista de negação seria mais fácil de furar com um caminho não previsto.
|
|
173
162
|
"""
|
|
174
|
-
store = physical(uniflow_store(home))
|
|
175
163
|
for target in targets:
|
|
176
164
|
path = physical(target["path"])
|
|
177
165
|
agents_ = target.get("agents") or []
|
|
178
166
|
if "custom" in agents_:
|
|
179
167
|
continue # --dest é escolha explícita e consciente do usuário
|
|
180
|
-
if path == store and agents_ == ["uniflow"]:
|
|
181
|
-
continue
|
|
182
168
|
if plausible_skills_dir(path, agents_):
|
|
183
169
|
continue
|
|
184
170
|
# Uma área protegida tem razão PRÓPRIA e melhor: dizer "não parece diretório de skills" ali
|
|
@@ -188,7 +174,7 @@ def refuse_implausible_destination(targets, home):
|
|
|
188
174
|
try:
|
|
189
175
|
if shielded(Path(path), home):
|
|
190
176
|
raise CrowError("protected_path", "O destino de " + ",".join(agents_) + " resolve para " + path +
|
|
191
|
-
", que é área protegida (prompt compartilhado
|
|
177
|
+
", que é área protegida (prompt compartilhado).")
|
|
192
178
|
except CrowError:
|
|
193
179
|
raise
|
|
194
180
|
except Exception: # noqa: BLE001 - se não der para avaliar, vale a razão genérica
|
|
@@ -199,27 +185,6 @@ def refuse_implausible_destination(targets, home):
|
|
|
199
185
|
"instalar, use --dest explicitamente.")
|
|
200
186
|
|
|
201
187
|
|
|
202
|
-
def refuse_store_as_cli_dir(targets, home):
|
|
203
|
-
"""Recusa um destino de CLI que resolve para o store do Uniflow.
|
|
204
|
-
|
|
205
|
-
Resolver symlink (0.16.0) tornou isso alcançável pela seleção de um CLI: um
|
|
206
|
-
~/.claude/skills -> ~/.config/uniflow/skills gravaria um pacote dentro do store, que vale para
|
|
207
|
-
TODOS os CLIs que o Uniflow projeta, não só o selecionado. Só vale na MUTAÇÃO: `destinations()`
|
|
208
|
-
é chamado por vários caminhos de leitura (status, doctor, avisos do uniflow) que inspecionam o
|
|
209
|
-
destino de outro agente, e levantar lá derrubaria um comando que nem ia gravar naquele lugar.
|
|
210
|
-
"""
|
|
211
|
-
store = physical(uniflow_store(home))
|
|
212
|
-
for target in targets:
|
|
213
|
-
others = [a for a in target.get("agents") or [] if a != "uniflow"]
|
|
214
|
-
if others and physical(target["path"]) == store:
|
|
215
|
-
# "custom" é o rótulo interno de --dest: dizer "o destino de custom" confunde quem não
|
|
216
|
-
# passou agente nenhum. A mensagem nomeia o que o usuário realmente escreveu.
|
|
217
|
-
quem = "--dest" if others == ["custom"] else ",".join(others)
|
|
218
|
-
raise CrowError("protected_path", "O destino de " + quem + " resolve para o store do Uniflow ("
|
|
219
|
-
+ store + "), que vale para todos os CLIs que ele projeta. Use --agents uniflow "
|
|
220
|
-
"para gravar no store.")
|
|
221
|
-
|
|
222
|
-
|
|
223
188
|
def inventory(home):
|
|
224
189
|
rows = []
|
|
225
190
|
for name, (_, project, binary) in AGENTS.items():
|
|
@@ -39,7 +39,7 @@ UNIT = "crow-skills-autoupdate"
|
|
|
39
39
|
FORMULA = "andersonflima/tap/crow-skills"
|
|
40
40
|
PACKAGE = "@andespindola/crow-skills"
|
|
41
41
|
HEADER = "# Gerenciado por crow-skills autoupdate. Não edite; use crow-skills autoupdate off."
|
|
42
|
-
AGENT_ORDER = ("claude", "codex", "gemini", "devin"
|
|
42
|
+
AGENT_ORDER = ("claude", "codex", "gemini", "devin")
|
|
43
43
|
HISTORY = 20
|
|
44
44
|
DAILY = {"Hour": 10, "Minute": 17}
|
|
45
45
|
UNSAFE = re.compile(r"[\"'\\$`\n%]")
|
|
@@ -310,21 +310,6 @@ def preview_self(install, git_pull):
|
|
|
310
310
|
return {"kind": "unknown", "result": "skipped", "reason": "instalação não reconhecida (brew, npm ou checkout git)"}
|
|
311
311
|
|
|
312
312
|
|
|
313
|
-
def broken_projections(agent, home, store, rows):
|
|
314
|
-
"""Uniflow links of this CLI whose store entry was edited or lost files: the CLI may not load them, so
|
|
315
|
-
upgrade reports them as needing the user (same as setup), with the command that restores the store."""
|
|
316
|
-
from .guide import broken_fix, store_broken
|
|
317
|
-
names = set()
|
|
318
|
-
if any(r["status"] == "uniflow_link" for r in rows):
|
|
319
|
-
bad = store_broken(home, store)
|
|
320
|
-
names |= {r["name"] for r in rows if r["status"] == "uniflow_link" and r["name"] in bad}
|
|
321
|
-
if agent == "codex":
|
|
322
|
-
from . import projection
|
|
323
|
-
names |= {n for n, kind in projection.links(home, store).items() if kind == "modified"}
|
|
324
|
-
return [{"name": n, "status": "uniflow_link_modified", "fix": broken_fix(n)} for n in sorted(names)]
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
313
|
def catalog_gap(agent, home, source, rows, projected, broken):
|
|
329
314
|
"""What this agent is missing from the catalog, and whether upgrade may install it.
|
|
330
315
|
|
|
@@ -403,34 +388,6 @@ def reinstall_behaviour(home, entry, dry_run, trust):
|
|
|
403
388
|
entry["transaction"] = data.get("transaction")
|
|
404
389
|
|
|
405
390
|
|
|
406
|
-
def codex_duplicates(home, store, entry, names, dry_run):
|
|
407
|
-
"""Codex sees Uniflow's links in its own dir: those names are not updated there, and intact managed copies
|
|
408
|
-
left in the legacy shared folder are removed through uninstall against THAT folder (backup, transaction).
|
|
409
|
-
Returns the names to update."""
|
|
410
|
-
from . import projection
|
|
411
|
-
from .cli import parser, run as cli_run
|
|
412
|
-
seen = projection.projected("codex", home, store)
|
|
413
|
-
if not seen:
|
|
414
|
-
return names
|
|
415
|
-
dupes = projection.duplicates(home, store)
|
|
416
|
-
entry["via_uniflow"] = sorted(set(entry["via_uniflow"]) | seen)
|
|
417
|
-
entry["kept"] = [k for k in entry["kept"] if k["name"] not in seen]
|
|
418
|
-
entry["kept"] += [{"name": n, "status": "duplicate_modified"} for n in dupes["modified"]]
|
|
419
|
-
entry["kept"] += [{"name": n, "status": "duplicate_shared"} for n in dupes["shared"]]
|
|
420
|
-
if dupes["remove"]:
|
|
421
|
-
# --dest na pasta legada: em ~/.codex/skills esses nomes são links do Uniflow e o uninstall recusaria.
|
|
422
|
-
argv = ["uninstall", *dupes["remove"], "--dest", str(legacy_shared_dir(home)), "--json", "--home", str(home),
|
|
423
|
-
"--dry-run" if dry_run else "--yes", "--no-integrations"]
|
|
424
|
-
try:
|
|
425
|
-
data = cli_run(parser().parse_args(argv))
|
|
426
|
-
entry["removed"] = [{"name": c["name"], "reason": "removida: duplicava a projeção do Uniflow"}
|
|
427
|
-
for c in data["changes"] if c["action"] != "unchanged"]
|
|
428
|
-
entry["removal_transaction"] = data.get("transaction")
|
|
429
|
-
except CrowError as exc:
|
|
430
|
-
entry["error"] = {"code": exc.code, "message": str(exc)}
|
|
431
|
-
return [n for n in names if n not in seen]
|
|
432
|
-
|
|
433
|
-
|
|
434
391
|
def skills_update(home, trust=True, dry_run=False):
|
|
435
392
|
"""update --global for every agent with managed installs; modified skills are kept and reported.
|
|
436
393
|
|
|
@@ -457,73 +414,24 @@ def skills_update(home, trust=True, dry_run=False):
|
|
|
457
414
|
except (CrowError, OSError) as exc:
|
|
458
415
|
results.append({"agent": agent, "error": {"code": getattr(exc, "code", "io_error"), "message": str(exc)}})
|
|
459
416
|
continue
|
|
460
|
-
from .guide import store_managed
|
|
461
|
-
store_ok = store_managed(home, store) if any(r["status"] == "uniflow_link" for r in rows) else set()
|
|
462
|
-
projected = sorted(r["name"] for r in rows if r["status"] == "uniflow_link" and r["name"] in store_ok)
|
|
463
|
-
if agent == "codex":
|
|
464
|
-
from . import projection
|
|
465
|
-
projected = sorted(set(projected) | projection.projected("codex", home, store)) # ~/.codex/skills links
|
|
466
|
-
broken = broken_projections(agent, home, store, rows)
|
|
467
417
|
rows = [r for r in rows if r["name"] and r["revision"]]
|
|
468
418
|
if not rows:
|
|
469
|
-
if projected or broken:
|
|
470
|
-
# Every crow skill here comes from Uniflow's store (updated as agent uniflow): only the guide,
|
|
471
|
-
# integrations and hooks of this CLI are refreshed.
|
|
472
|
-
from . import machine
|
|
473
|
-
entry = {"agent": agent, "target": target["path"], "updated": [], "unchanged": 0, "kept": list(broken), "error": None,
|
|
474
|
-
"via_uniflow": projected}
|
|
475
|
-
gap = catalog_gap(agent, home, current, rows, projected, broken)
|
|
476
|
-
if gap["missing"]:
|
|
477
|
-
entry["newInCatalog"] = gap["missing"]
|
|
478
|
-
if dry_run:
|
|
479
|
-
entry["dry_run"] = True
|
|
480
|
-
try:
|
|
481
|
-
if agent == "codex" and "crow-behaviour" in [k["name"] for k in broken]:
|
|
482
|
-
reinstall_behaviour(home, entry, dry_run, trust)
|
|
483
|
-
extras = machine.refresh_extras(agent, home, dry_run, trust)
|
|
484
|
-
entry["guides"] = (extras.get("guides") or {}).get("transaction")
|
|
485
|
-
except CrowError as exc:
|
|
486
|
-
entry["error"] = {"code": exc.code, "message": str(exc)}
|
|
487
|
-
except (OSError, ValueError, KeyError, TypeError):
|
|
488
|
-
entry["error"] = {"code": "io_error", "message": "Falha de leitura/gravação; nada concluído para este agente."}
|
|
489
|
-
if gap["install"] and not entry.get("error"):
|
|
490
|
-
install_new(home, entry, gap["install"], agent, dry_run, trust)
|
|
491
|
-
if agent == "codex" and not entry.get("error"):
|
|
492
|
-
# Ramo mais provável de quem vinha da 0.14.0: no diretório do Codex só há links do
|
|
493
|
-
# Uniflow, e TODAS as cópias do codex estão na pasta compartilhada. Sem migrar aqui,
|
|
494
|
-
# uma máquina que só roda autoupdate nunca sai daquela pasta.
|
|
495
|
-
machine.migrate_legacy(None, home, store, entry, dry_run, planned=projected)
|
|
496
|
-
results.append(entry)
|
|
497
419
|
continue
|
|
498
420
|
records = store.state(target["path"])["installed"]
|
|
499
421
|
names, kept = [], []
|
|
500
422
|
for r in rows:
|
|
501
|
-
if r["status"] not in ("installed",
|
|
423
|
+
if r["status"] not in ("installed",):
|
|
502
424
|
kept.append({"name": r["name"], "status": r["status"]})
|
|
503
425
|
elif current is not None and not same_source(records.get(r["name"], {}), current):
|
|
504
426
|
kept.append({"name": r["name"], "status": "other_origin"}) # update --source or install switches it
|
|
505
427
|
else:
|
|
506
428
|
names.append(r["name"])
|
|
507
|
-
entry = {"agent": agent, "target": target["path"], "updated": [], "unchanged": 0, "kept": kept
|
|
508
|
-
|
|
509
|
-
gap = catalog_gap(agent, home, current, rows, projected, broken)
|
|
429
|
+
entry = {"agent": agent, "target": target["path"], "updated": [], "unchanged": 0, "kept": kept, "error": None}
|
|
430
|
+
gap = catalog_gap(agent, home, current, rows, (), ())
|
|
510
431
|
if gap["missing"]:
|
|
511
432
|
entry["newInCatalog"] = gap["missing"]
|
|
512
433
|
if dry_run:
|
|
513
434
|
entry["dry_run"] = True
|
|
514
|
-
if agent == "codex":
|
|
515
|
-
names = codex_duplicates(home, store, entry, names, dry_run)
|
|
516
|
-
if "crow-behaviour" in [k["name"] for k in broken] and "crow-behaviour" not in [r["name"] for r in rows]:
|
|
517
|
-
try:
|
|
518
|
-
reinstall_behaviour(home, entry, dry_run, trust)
|
|
519
|
-
except CrowError as exc:
|
|
520
|
-
entry["error"] = {"code": exc.code, "message": str(exc)}
|
|
521
|
-
if not names and not entry.get("error"):
|
|
522
|
-
from . import machine
|
|
523
|
-
try:
|
|
524
|
-
machine.refresh_extras(agent, home, dry_run, trust)
|
|
525
|
-
except CrowError as exc:
|
|
526
|
-
entry["error"] = {"code": exc.code, "message": str(exc)}
|
|
527
435
|
if names:
|
|
528
436
|
try:
|
|
529
437
|
argv = ["update", *names, "--agents", agent, "--global", "--dry-run" if dry_run else "--yes", "--json",
|