@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 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, auto-update ou reconfiguração de agentes. 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.
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": "Aplique um modo de atuação orientado a orquestração, profundidade e verificação em tarefas substantivas. Decomponha, delegue com as capacidades reais do agente e sintetize evidências. Em conversa ou ajustes triviais, responda diretamente; não simule agentes nem altere permissões.",
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": []
@@ -1,3 +1,3 @@
1
1
  """Portable, local-first installer for the crow-skills catalogue."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.4.0"
@@ -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
- result = {"installations": rows, "pending_transactions": pending,
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), agents=inventory(home))
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
- summary = {"transactions": [{"id": t["id"], "targets": list(t["before"])} for t in transactions]}
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 if getattr(args, "dry_run", False) else store.apply(plan)
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"]: