@andespindola/crow-skills 0.1.0 → 0.4.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 +7 -3
- package/catalog.json +1 -1
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/cli.py +261 -8
- package/crow_skills/guide.py +585 -0
- package/crow_skills/integrations.py +520 -0
- package/crow_skills/progress.py +667 -0
- package/crow_skills/storage.py +6 -0
- package/docs/cli-security.md +2 -0
- package/docs/cli.md +104 -4
- package/package.json +1 -1
- package/release.json +39 -14
- package/skills/crow-behaviour/SKILL.md +24 -19
- package/skills/crow-behaviour/agents/openai.yaml +1 -1
- package/skills/crow-behaviour/references/orchestration.md +19 -9
- package/skills/crow-behaviour/references/progress-ledger.md +83 -0
- package/skills/crow-behaviour/references/quality-patterns.md +22 -2
- package/skills/crow-behaviour/scripts/crow_ledger.py +279 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Skills com escopo explícito, conhecimento especializado preservado e fluxos comuns reutilizáveis.
|
|
4
4
|
|
|
5
|
+
**Versão 0.4.0** ([mudanças](CHANGELOG.md)): crow-behaviour com paridade de orquestração, ledger de progresso, `crow-skills progress` e integrações de interface por agente.
|
|
6
|
+
|
|
5
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).
|
|
6
8
|
|
|
7
9
|
## Uso
|
|
@@ -12,7 +14,7 @@ Skills com escopo explícito, conhecimento especializado preservado e fluxos com
|
|
|
12
14
|
4. Entregue o artefato com alvo/revisão, evidência e pendências. Desconhecido não significa aprovado.
|
|
13
15
|
5. Carregue outra skill só quando houver decisão distinta a tomar. Handoffs são opcionais, não dependências ocultas.
|
|
14
16
|
|
|
15
|
-
Para adotar uma atuação mais coordenada em qualquer domínio, use `crow-behaviour`: tarefas substantivas recebem plano, delegação suportada, verificação e síntese; conversa e ajustes triviais continuam diretos. A skill adapta o comportamento às ferramentas reais, inclusive execução sequencial, sem depender do Workflow do Claude. Ela não muda o modelo/effort nem força carregamento automático pelo host. [Escopo e evidência](analysis/crow-behaviour.md).
|
|
17
|
+
Para adotar uma atuação mais coordenada em qualquer domínio, use `crow-behaviour`: tarefas substantivas recebem plano, delegação suportada, verificação e síntese; conversa e ajustes triviais continuam diretos. A skill adapta o comportamento às ferramentas reais, inclusive execução sequencial, sem depender do Workflow do Claude. Em tarefa substantiva ela mantém um ledger de progresso (`.crow/runs/`), mostra um resumo por fase no chat e, dentro do tmux, um painel ao vivo com `crow-skills progress --watch`. Ela não muda o modelo/effort nem força carregamento automático pelo host. [Escopo e evidência](analysis/crow-behaviour.md).
|
|
16
18
|
|
|
17
19
|
Exemplos:
|
|
18
20
|
- Plataforma nova: `crow-platform` coordena; `crow-discovery` e `crow-spec` definem o que falta; as especialistas implementam/verificam por fatia.
|
|
@@ -25,9 +27,11 @@ Exemplos:
|
|
|
25
27
|
|
|
26
28
|
## Instalação e portabilidade
|
|
27
29
|
|
|
28
|
-
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.
|
|
30
|
+
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. 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 line do Claude Code e do Codex, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga.
|
|
29
31
|
|
|
30
32
|
```sh
|
|
33
|
+
npm install -g @andespindola/crow-skills@0.4.0
|
|
34
|
+
crow-skills progress
|
|
31
35
|
./bin/crow-skills list
|
|
32
36
|
./bin/crow-skills install crow-behaviour --agents claude,codex --global --dry-run
|
|
33
37
|
./bin/crow-skills install crow-behaviour --agents claude,codex --global --yes --json
|
|
@@ -36,7 +40,7 @@ O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](do
|
|
|
36
40
|
|
|
37
41
|
Também é possível copiar/referenciar manualmente as pastas escolhidas no diretório suportado pelo host. Para symlink manual, mantenha o checkout no mesmo caminho. O CLI v0.1 só instala cópias; não sobrescreve conflitos ou edições sem opção explícita e backup.
|
|
38
42
|
|
|
39
|
-
Os nomes têm prefixo `crow-` para coexistir com o catálogo anterior. Não há instalação global automática
|
|
43
|
+
Os nomes têm prefixo `crow-` para coexistir com o catálogo anterior. Não há instalação global automática nem auto-update; em `--global`, a única reconfiguração de agentes é o guia aditivo e as integrações de interface, ambos desligáveis (`--no-guide`, `--no-integrations`). O CLI mostra complementaridade por role/owner/handoffs e overlays, mas só instala as selecionadas; `--with-handoffs` é opt-in para handoffs diretos. Operações especializadas podem depender de ferramentas explicitamente documentadas: `crow-interface`, por exemplo, inclui adaptador de motor versionado e instalação explícita dessa dependência. A descoberta depende do runtime; presença do arquivo não prova carregamento.
|
|
40
44
|
|
|
41
45
|
Cada pacote mantém suas referências locais e declara dependências operacionais. Os nomes de próximos responsáveis são sugestões, não downloads implícitos. Sem ferramenta necessária, entregue somente o que puder comprovar e descreva a capacidade não executada, em vez de alegar equivalência.
|
|
42
46
|
|
package/catalog.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"name": "crow-behaviour",
|
|
14
14
|
"title": "Atuação coordenada",
|
|
15
|
-
"description": "
|
|
15
|
+
"description": "Use PROATIVAMENTE, sem o usuário pedir, sempre que a tarefa for substantiva — multi-etapa, com exploração/decisões, mexendo em vários arquivos ou componentes, ou exigindo verificação antes de entregar (implementar uma feature, refatorar, depurar, auditar, revisar, migrar, pesquisar a fundo). Ao ativar: decomponha o trabalho, delegue com as capacidades reais do runtime, verifique de forma adversarial e sintetize evidências. NÃO ative em conversa, explicação curta ou ajuste trivial/local — nesses casos responda direto. Não simule agentes nem altere permissões.",
|
|
16
16
|
"role": "coordinator",
|
|
17
17
|
"owns": "execution-ledger",
|
|
18
18
|
"next": []
|
package/crow_skills/__init__.py
CHANGED
package/crow_skills/cli.py
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
"""Human and agent CLI. Noninteractive mutations require explicit --yes."""
|
|
2
2
|
import argparse
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
import sys
|
|
6
7
|
|
|
7
8
|
from . import __version__
|
|
9
|
+
from . import guide
|
|
10
|
+
from . import integrations
|
|
11
|
+
from . import progress
|
|
8
12
|
from .agents import AGENTS, destinations, inventory
|
|
9
13
|
from .source import CrowError, Source, valid_name
|
|
10
14
|
from .storage import Store, checked
|
|
11
15
|
|
|
12
16
|
DEFAULT_SOURCE = Path(__file__).resolve().parents[1]
|
|
13
|
-
MUTATIONS = {"install", "update", "uninstall", "rollback", "recover"}
|
|
17
|
+
MUTATIONS = {"install", "update", "uninstall", "rollback", "recover", "guide", "integrations"}
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
class Parser(argparse.ArgumentParser):
|
|
@@ -51,6 +55,19 @@ def parser():
|
|
|
51
55
|
child.add_argument("--replace", action="store_true", help="Autorizar substituição com backup de pacote não gerenciado")
|
|
52
56
|
child.add_argument("--with-handoffs", action="store_true", help="Incluir apenas handoffs diretos opcionais do catálogo")
|
|
53
57
|
child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição/remoção de edições locais")
|
|
58
|
+
child.add_argument("--no-guide", action="store_true", help="Não aplicar/atualizar/remover o guia do agente (--global)")
|
|
59
|
+
child.add_argument("--no-integrations", action="store_true",
|
|
60
|
+
help="Não aplicar/remover as integrações de interface (status line, tmux) em --global")
|
|
61
|
+
child = sub.add_parser("guide", parents=[common], help="Instruções gerenciadas para o agente usar as skills instaladas")
|
|
62
|
+
child.add_argument("action", choices=("install", "status", "uninstall"))
|
|
63
|
+
child.add_argument("--replace", action="store_true", help="Autorizar backup e substituição de conteúdo não gerenciado no caminho do guia")
|
|
64
|
+
child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição/remoção de guia editado")
|
|
65
|
+
child = sub.add_parser("integrations", parents=[common], help="Status line/tmux que mostram a execução crow-behaviour ativa")
|
|
66
|
+
child.add_argument("action", choices=("install", "status", "uninstall"))
|
|
67
|
+
child = sub.add_parser("progress", parents=[common], help="Painel de progresso da execução crow-behaviour (ledger, barras, tokens)")
|
|
68
|
+
child.add_argument("--watch", action="store_true", help="Redesenhar até a execução terminar ou Ctrl-C")
|
|
69
|
+
child.add_argument("--run", help="ID da execução; padrão: a ativa mais recente do diretório")
|
|
70
|
+
child.add_argument("--cwd", help="Diretório da execução; padrão: o atual")
|
|
54
71
|
for name in ("rollback", "recover"):
|
|
55
72
|
child = sub.add_parser(name, parents=[common])
|
|
56
73
|
child.add_argument("--transaction", required=name == "rollback")
|
|
@@ -113,6 +130,197 @@ def relationship_view(source, name, store, targets):
|
|
|
113
130
|
"meaning": "Handoffs e overlays são complementos opcionais, não dependências nem ordem automática de execução."}
|
|
114
131
|
|
|
115
132
|
|
|
133
|
+
def guide_command(args, home, store, interactive):
|
|
134
|
+
if getattr(args, "project", None) or getattr(args, "dest", None):
|
|
135
|
+
raise CrowError("unsupported_guide", "Guias só têm mecanismo nativo verificado no escopo do usuário; use --global. "
|
|
136
|
+
"Escopo de projeto e --dest não são suportados.")
|
|
137
|
+
agents = getattr(args, "agents", None)
|
|
138
|
+
if args.action == "status":
|
|
139
|
+
rows = guide.status(store, home, guide.select(agents) if agents else list(guide.MECHANISMS))
|
|
140
|
+
return {"guides": rows, "pending_transactions": [t["id"] for t in store.pending()], "activation_verified": False}
|
|
141
|
+
if not getattr(args, "global_scope", False):
|
|
142
|
+
raise CrowError("usage", "guide " + args.action + " exige --global explícito (único escopo verificado).")
|
|
143
|
+
if not agents and interactive:
|
|
144
|
+
agents = prompt("Agentes separados por vírgula (" + ",".join(guide.MECHANISMS) + "):")
|
|
145
|
+
selected = guide.select(agents)
|
|
146
|
+
source = None
|
|
147
|
+
if args.action == "install":
|
|
148
|
+
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"), getattr(args, "fetch", False))
|
|
149
|
+
plan = guide.plan(store, args.action, selected, home, source, args.replace, args.overwrite_modified)
|
|
150
|
+
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
151
|
+
"additive": True, "activation_verified": False}
|
|
152
|
+
if getattr(args, "dry_run", False):
|
|
153
|
+
summary["preview"] = plan["previews"]
|
|
154
|
+
if plan["items"]:
|
|
155
|
+
authorize(args, interactive, summary)
|
|
156
|
+
txid = None if getattr(args, "dry_run", False) else guide.apply(store, plan, home)
|
|
157
|
+
return {**summary, "transaction": txid, "dry_run": getattr(args, "dry_run", False),
|
|
158
|
+
"applied": not getattr(args, "dry_run", False),
|
|
159
|
+
"reload_hint": ("Somente preview; nada gravado." if getattr(args, "dry_run", False) else
|
|
160
|
+
"Abra uma sessão nova do CLI para carregar as instruções; o carregamento não é verificado por este comando.")}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
GUIDE_HINT = (" O guia do agente é aplicado junto com as skills em --global; revise e use --replace ou "
|
|
164
|
+
"--overwrite-modified conforme o caso, ou --no-guide para não alterar o guia. Nada foi alterado.")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def guide_sync(args, home, store, command, targets, plan, source):
|
|
168
|
+
"""Plan the guide changes that follow a skills plan, before anything is written.
|
|
169
|
+
|
|
170
|
+
Returns (report, guide plan or None). A refused guide raises, so the command
|
|
171
|
+
fails as a whole with no partial state.
|
|
172
|
+
"""
|
|
173
|
+
report = {"changes": [], "skipped": [], "transaction": None}
|
|
174
|
+
selected = [a for t in targets for a in t["agents"]]
|
|
175
|
+
if getattr(args, "no_guide", False):
|
|
176
|
+
report["skipped"] = [{"agent": a, "reason": "--no-guide: guia não alterado."} for a in selected]
|
|
177
|
+
return report, None
|
|
178
|
+
if getattr(args, "project", None) or getattr(args, "dest", None):
|
|
179
|
+
report["skipped"] = [{"agent": a, "reason": "Sem mecanismo de guia verificado fora do escopo do usuário; "
|
|
180
|
+
"guia aplicado só com --global."} for a in selected]
|
|
181
|
+
return report, None
|
|
182
|
+
for agent in selected:
|
|
183
|
+
if agent not in guide.MECHANISMS:
|
|
184
|
+
report["skipped"].append({"agent": agent, "reason": guide.UNVERIFIED.get(agent, "Sem mecanismo de guia verificado.")})
|
|
185
|
+
state = guide.load_state(store)
|
|
186
|
+
touched = {}
|
|
187
|
+
for item in plan["items"]:
|
|
188
|
+
added, removed = touched.setdefault(os.path.abspath(item["target"]), (set(), set()))
|
|
189
|
+
(removed if item["new"] is None else added).add(item["name"])
|
|
190
|
+
actions, skills_after = {}, {}
|
|
191
|
+
for agent, mech in guide.MECHANISMS.items():
|
|
192
|
+
path = os.path.abspath(destinations(agent, home)[0]["path"])
|
|
193
|
+
record = state["guides"].get(agent)
|
|
194
|
+
if agent not in selected and not (record and path in touched):
|
|
195
|
+
continue
|
|
196
|
+
added, removed = touched.get(path, (set(), set()))
|
|
197
|
+
after = sorted((set(guide.managed_skills(agent, home, store)) | added) - removed)
|
|
198
|
+
if mech["kind"] == "toml" and guide.tomllib is None:
|
|
199
|
+
report["skipped"].append({"agent": agent, "reason": "Python 3.11+ (tomllib) é necessário para editar config.toml; guia não alterado."})
|
|
200
|
+
elif "crow-behaviour" in after and (record or command != "uninstall"):
|
|
201
|
+
actions[agent], skills_after[agent] = "install", after
|
|
202
|
+
elif record and "crow-behaviour" in removed:
|
|
203
|
+
actions[agent] = "uninstall"
|
|
204
|
+
elif record and "crow-behaviour" not in after:
|
|
205
|
+
report["skipped"].append({"agent": agent, "reason": "crow-behaviour ausente; guia mantido. Confira crow-skills guide status."})
|
|
206
|
+
elif not record and command != "uninstall":
|
|
207
|
+
report["skipped"].append({"agent": agent, "reason": "crow-behaviour não ficará instalada para " + agent + "; guia não aplicado."})
|
|
208
|
+
if not actions:
|
|
209
|
+
return report, None
|
|
210
|
+
if source is None and "install" in actions.values():
|
|
211
|
+
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"))
|
|
212
|
+
try:
|
|
213
|
+
gplan = guide.plan(store, actions, list(actions), home, source, getattr(args, "replace", False),
|
|
214
|
+
args.overwrite_modified, skills_after)
|
|
215
|
+
except CrowError as exc:
|
|
216
|
+
raise CrowError(exc.code, str(exc) + GUIDE_HINT) from exc
|
|
217
|
+
report["changes"] = gplan["changes"]
|
|
218
|
+
report["revision"] = source.revision if source else None
|
|
219
|
+
if getattr(args, "dry_run", False):
|
|
220
|
+
report["preview"] = gplan["previews"]
|
|
221
|
+
return report, gplan
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def apply_chain(store, plan, gplan, home):
|
|
225
|
+
"""Skills first, then the guide; a failed guide rolls the skills back."""
|
|
226
|
+
txid = store.apply(plan)
|
|
227
|
+
if gplan is None:
|
|
228
|
+
return txid, None
|
|
229
|
+
try:
|
|
230
|
+
return txid, guide.apply(store, gplan, home, linked=txid)
|
|
231
|
+
except (Exception, KeyboardInterrupt) as exc:
|
|
232
|
+
if txid:
|
|
233
|
+
try:
|
|
234
|
+
store.rollback(txid)
|
|
235
|
+
except CrowError as inner:
|
|
236
|
+
raise CrowError("partial_state", "Guia falhou e as skills da transação " + txid + " não puderam ser desfeitas (" +
|
|
237
|
+
str(inner) + "). Execute crow-skills recover; ele desfaz o guia e as skills ligadas.") from exc
|
|
238
|
+
raise
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def behaviour_agents(home, store, plan=None):
|
|
242
|
+
"""Agents with a verified guide mechanism that have crow-behaviour after the plan applies."""
|
|
243
|
+
touched = {}
|
|
244
|
+
for item in (plan or {}).get("items", []):
|
|
245
|
+
added, removed = touched.setdefault(os.path.abspath(item["target"]), (set(), set()))
|
|
246
|
+
(removed if item["new"] is None else added).add(item["name"])
|
|
247
|
+
result = []
|
|
248
|
+
for agent in guide.MECHANISMS:
|
|
249
|
+
path = os.path.abspath(destinations(agent, home)[0]["path"])
|
|
250
|
+
added, removed = touched.get(path, (set(), set()))
|
|
251
|
+
if "crow-behaviour" in (set(guide.managed_skills(agent, home, store)) | added) - removed:
|
|
252
|
+
result.append(agent)
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def integrations_preview(args, home, store, plan):
|
|
257
|
+
"""Integrations follow the skills in --global unless --no-integrations ("disabled" otherwise)."""
|
|
258
|
+
if getattr(args, "no_integrations", False):
|
|
259
|
+
return {"changes": [], "skipped": [{"integration": "*", "reason": "--no-integrations: integrações não alteradas."}], "disabled": True}
|
|
260
|
+
if getattr(args, "project", None) or getattr(args, "dest", None):
|
|
261
|
+
return {"changes": [], "skipped": [{"integration": "*", "reason": "Integrações de interface só existem no escopo do usuário (--global)."}],
|
|
262
|
+
"disabled": True}
|
|
263
|
+
return integrations.sync(store, home, integration_agents(args, home, store, plan), dry_run=True)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def integration_agents(args, home, store, plan=None, selected=None):
|
|
267
|
+
"""Agents whose integrations should exist: crow-behaviour present and either selected now
|
|
268
|
+
or already integrated (like the guide, a shared-root install does not opt codex in)."""
|
|
269
|
+
if selected is None:
|
|
270
|
+
selected = [a for t in destinations(getattr(args, "agents", None), home) for a in t["agents"]] if getattr(args, "agents", None) else []
|
|
271
|
+
try:
|
|
272
|
+
owners = {a for name in integrations.load_state(store)["integrations"] for a in integrations.OWNERS.get(name, ())}
|
|
273
|
+
except CrowError:
|
|
274
|
+
owners = set()
|
|
275
|
+
return [a for a in behaviour_agents(home, store, plan) if a in selected or a in owners]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def integrations_command(args, home, store, interactive):
|
|
279
|
+
if args.action == "status":
|
|
280
|
+
return {"integrations": integrations.status(store, home)}
|
|
281
|
+
if not getattr(args, "global_scope", False):
|
|
282
|
+
raise CrowError("usage", "integrations " + args.action + " exige --global explícito (único escopo suportado).")
|
|
283
|
+
wanted = []
|
|
284
|
+
if args.action == "install":
|
|
285
|
+
wanted = integration_agents(args, home, store) if getattr(args, "agents", None) else behaviour_agents(home, store)
|
|
286
|
+
if args.action == "install" and not wanted:
|
|
287
|
+
raise CrowError("guide_requires_skill", "crow-behaviour não está instalada para nenhum agente; instale antes: "
|
|
288
|
+
"crow-skills install crow-behaviour --agents claude --global --yes")
|
|
289
|
+
preview = integrations.sync(store, home, wanted, dry_run=True)
|
|
290
|
+
if preview["changes"]:
|
|
291
|
+
authorize(args, interactive, preview)
|
|
292
|
+
if getattr(args, "dry_run", False):
|
|
293
|
+
return {**preview, "applied": False}
|
|
294
|
+
return {**integrations.sync(store, home, wanted), "applied": True}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def progress_command(args, home):
|
|
298
|
+
cwd = getattr(args, "cwd", None) or os.getcwd()
|
|
299
|
+
if args.watch:
|
|
300
|
+
if getattr(args, "json", False):
|
|
301
|
+
raise CrowError("usage", "--watch não combina com --json.")
|
|
302
|
+
progress.watch(cwd, home, args.run)
|
|
303
|
+
return None
|
|
304
|
+
run, usage = progress.snapshot(cwd, home, args.run)
|
|
305
|
+
data = progress.as_json(run, usage)
|
|
306
|
+
data["text"] = progress.render(run, usage, color=False, unicode=progress.unicode_ok()) if run else None
|
|
307
|
+
return data
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def resync_integrations(home, store):
|
|
311
|
+
"""After rollback/recover, drop integrations whose agents no longer have crow-behaviour."""
|
|
312
|
+
try:
|
|
313
|
+
if integrations.load_state(store)["integrations"]:
|
|
314
|
+
integrations.sync(store, home, integration_agents(None, home, store, selected=[]))
|
|
315
|
+
except CrowError:
|
|
316
|
+
pass # Reported by status/doctor; never turns a completed rollback into a failure.
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def linked_guides(store, txid):
|
|
320
|
+
"""Committed guide transactions that completed the given skills transaction."""
|
|
321
|
+
return [t["id"] for t in store.transactions() if t.get("kind") == "guide" and t.get("linked") == txid and t["phase"] == "committed"]
|
|
322
|
+
|
|
323
|
+
|
|
116
324
|
def run(args):
|
|
117
325
|
if args.version:
|
|
118
326
|
return {"version": __version__}
|
|
@@ -121,18 +329,32 @@ def run(args):
|
|
|
121
329
|
home = checked(getattr(args, "home", Path.home()))
|
|
122
330
|
store = Store(home)
|
|
123
331
|
interactive = sys.stdin.isatty() and not getattr(args, "non_interactive", False) and not getattr(args, "json", False)
|
|
124
|
-
if getattr(args, "fetch", False) and (getattr(args, "dry_run", False) or args.command not in ("install", "update", "list", "info", "related")):
|
|
332
|
+
if getattr(args, "fetch", False) and (getattr(args, "dry_run", False) or args.command not in ("install", "update", "list", "info", "related", "guide")):
|
|
125
333
|
raise CrowError("usage", "--fetch requer um comando com fonte e não pode ser combinado com --dry-run.")
|
|
126
334
|
if args.command == "agents":
|
|
127
335
|
return {"agents": inventory(home), "note": "Detecção por PATH/diretórios não comprova carregamento da skill."}
|
|
336
|
+
if args.command == "guide":
|
|
337
|
+
return guide_command(args, home, store, interactive)
|
|
338
|
+
if args.command == "integrations":
|
|
339
|
+
return integrations_command(args, home, store, interactive)
|
|
340
|
+
if args.command == "progress":
|
|
341
|
+
return progress_command(args, home)
|
|
128
342
|
if args.command in ("status", "doctor"):
|
|
129
343
|
targets = scope(args, home, read=True)
|
|
130
344
|
rows = store.status(targets)
|
|
131
345
|
pending = [t["id"] for t in store.pending()]
|
|
132
|
-
|
|
346
|
+
global_view = not (getattr(args, "project", None) or getattr(args, "dest", None))
|
|
347
|
+
names = [n.strip() for n in getattr(args, "agents", "").split(",")] if getattr(args, "agents", None) else list(guide.MECHANISMS)
|
|
348
|
+
guides = guide.status(store, home, names) if global_view else []
|
|
349
|
+
try:
|
|
350
|
+
ui = integrations.status(store, home) if global_view else []
|
|
351
|
+
except CrowError as exc:
|
|
352
|
+
ui = [{"integration": "*", "status": "unsafe", "note": str(exc)}]
|
|
353
|
+
result = {"installations": rows, "guides": guides, "integrations": ui, "pending_transactions": pending,
|
|
133
354
|
"activation_verified": False, "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
|
|
134
355
|
if args.command == "doctor":
|
|
135
|
-
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows),
|
|
356
|
+
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows + guides),
|
|
357
|
+
agents=inventory(home))
|
|
136
358
|
return result
|
|
137
359
|
if args.command in ("rollback", "recover"):
|
|
138
360
|
if any(getattr(args, k, None) for k in ("agents", "dest", "project", "global_scope")):
|
|
@@ -142,12 +364,29 @@ def run(args):
|
|
|
142
364
|
expected = ("prepared", "restoring") if args.command == "recover" else ("committed",)
|
|
143
365
|
if any(t["phase"] not in expected for t in transactions):
|
|
144
366
|
raise CrowError("invalid_transaction", "Transação incompatível com a operação.")
|
|
145
|
-
|
|
367
|
+
# install/update/uninstall chain a skills transaction and a guide transaction
|
|
368
|
+
# (journal field "linked"). Undo both together: the guide first on rollback;
|
|
369
|
+
# on recover, an interrupted guide also undoes the skills it completed.
|
|
370
|
+
follow = []
|
|
371
|
+
if args.command == "rollback":
|
|
372
|
+
before_ids = [g for txid in ids for g in linked_guides(store, txid)]
|
|
373
|
+
ids, transactions = before_ids + ids, [store.journal(t) for t in before_ids] + transactions
|
|
374
|
+
else:
|
|
375
|
+
follow = [t["linked"] for t in transactions if t.get("linked")
|
|
376
|
+
and store.journal(t["linked"])["phase"] == "committed"]
|
|
377
|
+
view = lambda t: {"id": t["id"], "kind": t.get("kind", "skills"),
|
|
378
|
+
"targets": [i["path"] for i in t["items"]] if t.get("kind") == "guide" else list(t["before"])}
|
|
379
|
+
summary = {"transactions": [view(t) for t in transactions]}
|
|
380
|
+
if follow:
|
|
381
|
+
summary["then_rollback"] = [view(store.journal(t)) for t in follow]
|
|
146
382
|
if transactions:
|
|
147
383
|
authorize(args, interactive, summary)
|
|
148
384
|
if not getattr(args, "dry_run", False):
|
|
149
385
|
for txid in ids:
|
|
150
386
|
store.rollback(txid, recovery=args.command == "recover")
|
|
387
|
+
for txid in follow:
|
|
388
|
+
store.rollback(txid)
|
|
389
|
+
resync_integrations(home, store)
|
|
151
390
|
return {**summary, "dry_run": getattr(args, "dry_run", False), "applied": not getattr(args, "dry_run", False)}
|
|
152
391
|
source = None
|
|
153
392
|
if args.command != "uninstall":
|
|
@@ -185,12 +424,23 @@ def run(args):
|
|
|
185
424
|
source.require(name)
|
|
186
425
|
names.extend(n for n in source.catalog[name]["next"] if n not in names)
|
|
187
426
|
plan = store.plan(args.command, names, targets, source, getattr(args, "replace", False), args.overwrite_modified)
|
|
427
|
+
guides, gplan = guide_sync(args, home, store, args.command, targets, plan, source)
|
|
428
|
+
ui = integrations_preview(args, home, store, plan)
|
|
188
429
|
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
189
430
|
"relationships": {n: source.related(n) for n in names} if source else {},
|
|
190
|
-
"handoffs_are_optional": True, "activation_verified": False}
|
|
191
|
-
if plan["items"]:
|
|
431
|
+
"guides": guides, "integrations": ui, "handoffs_are_optional": True, "activation_verified": False}
|
|
432
|
+
if plan["items"] or (gplan and gplan["items"]) or ui.get("changes"):
|
|
192
433
|
authorize(args, interactive, summary)
|
|
193
|
-
txid = None
|
|
434
|
+
txid = None
|
|
435
|
+
if not getattr(args, "dry_run", False):
|
|
436
|
+
txid, guides["transaction"] = apply_chain(store, plan, gplan, home)
|
|
437
|
+
if not ui.get("disabled"):
|
|
438
|
+
# Best effort after the skills/guide transactions; a refusal is reported, never undoes them.
|
|
439
|
+
try:
|
|
440
|
+
summary["integrations"] = integrations.sync(store, home, integration_agents(args, home, store))
|
|
441
|
+
except (CrowError, OSError) as exc:
|
|
442
|
+
summary["integrations"] = {"changes": [], "skipped": [{"integration": "*", "reason": "Integrações não aplicadas (" +
|
|
443
|
+
(str(exc) or "falha de gravação") + "); skills e guia mantidos."}]}
|
|
194
444
|
return {**summary, "transaction": txid, "dry_run": getattr(args, "dry_run", False),
|
|
195
445
|
"applied": not getattr(args, "dry_run", False),
|
|
196
446
|
"reload_hint": ("Somente preview; nada instalado." if getattr(args, "dry_run", False) else
|
|
@@ -201,6 +451,9 @@ def emit(command, data, machine):
|
|
|
201
451
|
envelope = {"schema_version": 1, "ok": True, "command": command, "data": data}
|
|
202
452
|
if machine:
|
|
203
453
|
print(json.dumps(envelope, ensure_ascii=False, separators=(",", ":")))
|
|
454
|
+
elif command == "progress":
|
|
455
|
+
if data is not None:
|
|
456
|
+
print(data["text"] or "crow: nenhuma execução registrada neste diretório.")
|
|
204
457
|
elif command == "list":
|
|
205
458
|
print("Fonte: " + data["revision"])
|
|
206
459
|
for item in data["skills"]:
|