@andespindola/crow-skills 0.27.0 → 0.28.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.
@@ -15,7 +15,6 @@ import shutil
15
15
  import stat
16
16
  import uuid
17
17
 
18
- from .agents import uniflow_store
19
18
  from .source import CrowError, MAX_FILE, MAX_PACKAGE, MAX_FILES, SHA, digest, file_entry, safe_relative, same_source, source_identity, valid_name
20
19
 
21
20
 
@@ -33,6 +32,8 @@ def resolved(path):
33
32
 
34
33
  def checked(path):
35
34
  path = Path(os.path.abspath(Path(path).expanduser()))
35
+ from .agents import reject_retired
36
+ reject_retired(path)
36
37
  for part in reversed((path, *path.parents)):
37
38
  if part.is_symlink():
38
39
  raise CrowError("unsafe_destination", "Symlink no destino/estado; use um caminho físico explícito.")
@@ -117,7 +118,7 @@ DISABLED = "SKILL.md.disabled"
117
118
 
118
119
 
119
120
  def disabled_variant(entries):
120
- """The same package as Uniflow leaves it after /skills disable (SKILL.md renamed)."""
121
+ """Historical disabled-package representation, retained only for transaction recovery."""
121
122
  if not entries or "SKILL.md" not in entries:
122
123
  return None
123
124
  result = dict(entries)
@@ -160,13 +161,22 @@ def same_manifest(a, b):
160
161
  return {k: v for k, v in a.items() if k != "target"} == {k: v for k, v in b.items() if k != "target"}
161
162
 
162
163
 
163
- def validate_state(data, target):
164
+ def validate_state(data, target, *, _historical=False):
164
165
  try:
165
166
  if (data["schema_version"] != 1 or data["target"] != str(target) or
166
167
  not isinstance(data["installed"], dict) or len(data["installed"]) > 1000):
167
168
  raise ValueError("schema")
168
169
  for name, record in data["installed"].items():
169
170
  valid_name(name)
171
+ hosts = record.get("agents", [])
172
+ supported = {"codex", "claude", "gemini", "devin", "custom", "shared"}
173
+ # Read-only journal enumeration may validate the former host's metadata,
174
+ # never live ownership or explicit recovery. Unknown hosts still fail closed.
175
+ if _historical:
176
+ supported.add("uniflow")
177
+ if (not isinstance(hosts, list) or not all(isinstance(host, str) for host in hosts)
178
+ or set(hosts) - supported):
179
+ raise ValueError("unsupported historical host; record preserved")
170
180
  validate_entries(record["entries"])
171
181
  if not SHA.fullmatch(record["revision"]) or digest(record["entries"]) != record["package_sha256"]:
172
182
  raise ValueError("hash")
@@ -208,8 +218,17 @@ class Store:
208
218
  except (CrowError, OSError, ValueError):
209
219
  continue
210
220
  recorded = data.get("target") if isinstance(data, dict) else None
211
- if isinstance(recorded, str) and recorded != str(target) and physical(recorded) == wanted:
212
- found.append(candidate)
221
+ if isinstance(recorded, str) and recorded != str(target):
222
+ try:
223
+ from .agents import reject_retired
224
+ reject_retired(recorded)
225
+ if any(set(record.get("agents", ())) - {"codex", "claude", "gemini", "devin", "custom", "shared"}
226
+ for record in data.get("installed", {}).values()):
227
+ continue
228
+ if physical(recorded) == wanted:
229
+ found.append(candidate)
230
+ except CrowError:
231
+ continue
213
232
  # Mais de um apelido para o MESMO caminho físico é ambíguo: escolher o primeiro do scandir
214
233
  # (ordem arbitrária de hash) podia reportar uma skill gerenciada como `unmanaged` e, com
215
234
  # --replace, sobrescrevê-la como se fosse lixo alheio — exatamente o dano que o apelido
@@ -247,9 +266,14 @@ class Store:
247
266
 
248
267
  def transactions(self):
249
268
  directory = checked(self.root / "transactions")
250
- return [self.journal(p.stem) for p in sorted(directory.glob("*.json"))] if directory.exists() else []
269
+ return [self.journal(p.stem, _historical=True) for p in sorted(directory.glob("*.json"))] if directory.exists() else []
270
+
271
+ def journal(self, txid, *, _historical=False):
272
+ """Validate Crow-owned metadata; explicit recovery remains native-only.
251
273
 
252
- def journal(self, txid):
274
+ Historical enumeration does not resolve or inspect saved destinations. Terminal
275
+ retired records remain visible, and active ones remain pending, not discarded.
276
+ """
253
277
  if not re.fullmatch(r"[a-f0-9]{32}", txid):
254
278
  raise CrowError("invalid_transaction", "ID de transação inválido.")
255
279
  data = read_json(self.root / "transactions" / (txid + ".json"))
@@ -261,6 +285,7 @@ class Store:
261
285
  raise ValueError("phase")
262
286
  if "linked" in data and not re.fullmatch(r"[a-f0-9]{32}", data["linked"] or ""):
263
287
  raise ValueError("linked")
288
+ unsupported_recovery = False
264
289
  targets = data["before"]
265
290
  if not isinstance(targets, dict) or set(targets) != set(data["after"]):
266
291
  raise ValueError("targets")
@@ -271,8 +296,17 @@ class Store:
271
296
  # TODOS os agentes por causa de um journal antigo.
272
297
  if target != os.path.abspath(target):
273
298
  raise ValueError("path")
274
- validate_state(before, target)
275
- validate_state(data["after"][target], target)
299
+ from .agents import reject_retired
300
+ try:
301
+ reject_retired(target) # Lexical only; no saved target access.
302
+ except CrowError:
303
+ if not _historical:
304
+ raise ValueError("unsupported recovery destination")
305
+ unsupported_recovery = True
306
+ for saved in (before, data["after"][target]):
307
+ validate_state(saved, target, _historical=_historical)
308
+ if any("uniflow" in record.get("agents", []) for record in saved["installed"].values()):
309
+ unsupported_recovery = True
276
310
  for item in data["items"]:
277
311
  if item["target"] not in targets:
278
312
  raise ValueError("item target")
@@ -284,6 +318,9 @@ class Store:
284
318
  validate_entries(item[key])
285
319
  except (ValueError, KeyError, TypeError) as exc:
286
320
  raise CrowError("invalid_transaction", "Registro transacional inconsistente; recuperação recusada.") from exc
321
+ if _historical and unsupported_recovery:
322
+ # An annotation on the returned copy is not a rewrite of the journal.
323
+ return dict(data, recovery_supported=False)
287
324
  return data
288
325
 
289
326
  def pending(self):
@@ -311,8 +348,7 @@ class Store:
311
348
  from .guide import shielded
312
349
  target = resolved(target)
313
350
  key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
314
- # Beside the discovery dir, unless that parent is protected (the Uniflow
315
- # config): then staging/backups stay in the private state root instead.
351
+ # Beside the discovery dir, unless its parent is protected; then use private state.
316
352
  base = self.root / "work" if shielded(target.parent / ".crow-skills", self.home) else target.parent / ".crow-skills"
317
353
  return checked(base / key / txid)
318
354
 
@@ -327,22 +363,6 @@ class Store:
327
363
  return checked(candidate)
328
364
  return primary
329
365
 
330
- def managed_target(self, resolved, name):
331
- """resolved is a package this CLI manages somewhere and its files match the manifest."""
332
- directory = self.root / "targets"
333
- if not directory.is_dir():
334
- return False
335
- for state_file in directory.glob("*/state.json"):
336
- try:
337
- data = read_json(state_file)
338
- validate_state(data, data["target"])
339
- record = data["installed"].get(name)
340
- place = Path(data["target"]) / name
341
- if record and os.path.realpath(place) == resolved and snapshot(place) == record["entries"]:
342
- return True
343
- except (CrowError, KeyError, TypeError, OSError):
344
- continue
345
- return False
346
366
 
347
367
  def plan(self, command, names, targets, source=None, replace=False, overwrite=False):
348
368
  if self.pending():
@@ -367,16 +387,7 @@ class Store:
367
387
  raise
368
388
  if command != "uninstall":
369
389
  raise CrowError("protected_path", str(exc) + " Instalação anterior a esta proteção: remova com "
370
- "crow-skills uninstall " + name + " --dest " + str(target) + " --yes e, se for para o "
371
- "Uniflow, reinstale com --agents uniflow --global.") from exc
372
- # Only the uniflow agent replaces Uniflow's import links; --dest keeps refusing links.
373
- # physical() e não resolved(): plan() tem um local chamado `resolved` mais abaixo, que
374
- # torna o nome local em toda a função e faria esta linha estourar UnboundLocalError.
375
- # O store do Uniflow em dotfiles tem o caminho resolvido, e comparar com o não resolvido
376
- # deixava o store inteiro não gerenciável (lacuna anterior à 0.16.0).
377
- from .agents import physical as _physical
378
- store_target = "uniflow" in target_info["agents"] and \
379
- str(target) == _physical(uniflow_store(self.home))
390
+ "crow-skills uninstall " + name + " --dest " + str(target) + " --yes e escolha um destino nativo suportado.") from exc
380
391
  before[key] = state
381
392
  after[key] = json.loads(json.dumps(state))
382
393
  selected = list(names) if names else list(state["installed"])
@@ -384,25 +395,11 @@ class Store:
384
395
  valid_name(name)
385
396
  record = state["installed"].get(name)
386
397
  dest = target / name
387
- link, managed_link = None, False
388
- if store_target and dest.is_symlink():
389
- # Uniflow imports a native copy as a link store/crow-x -> its realpath.
390
- link = os.readlink(dest)
391
- resolved = os.path.realpath(dest)
392
- managed_link = self.managed_target(resolved, name)
393
- if not (managed_link or replace or overwrite):
394
- raise CrowError("unmanaged_link", str(dest) + " é um link (do Uniflow) para " + resolved +
395
- ", que não é uma instalação gerenciada pelo crow-skills. Para trocar só o link por uma "
396
- "cópia gerenciada, sem tocar no alvo: crow-skills install " + name +
397
- " --agents uniflow --global --replace --yes. Ou remova o link você mesmo.")
398
- current = None
399
- else:
400
- current = snapshot(dest)
401
- disabled = bool(record and current is not None and current == disabled_variant(record["entries"]))
402
- effective = record["entries"] if disabled else current
398
+ current = snapshot(dest)
399
+ effective = current
403
400
  if command != "install" and record is None:
404
401
  raise CrowError("not_managed", "Skill não gerenciada neste destino: " + name)
405
- if record and effective != record["entries"] and not overwrite and not managed_link:
402
+ if record and effective != record["entries"] and not overwrite:
406
403
  raise CrowError("local_changes", "Skill modificada/ausente: " + name + ". Revise; --overwrite-modified autoriza preservar backup e substituir/remover.")
407
404
  if not record and current is not None and not replace:
408
405
  raise CrowError("unmanaged_conflict", "Destino já existe e não é gerenciado: " + name + ". Use --replace após revisar.")
@@ -414,18 +411,12 @@ class Store:
414
411
  if name not in packages:
415
412
  packages[name] = source.package(name)
416
413
  new = packages[name].entries
417
- if (record and command != "uninstall" and link is None and effective == new
414
+ if (record and command != "uninstall" and effective == new
418
415
  and record["revision"] == source.revision and record.get("source") == str(source.path)):
419
416
  output.append({"name": name, "target": key, "agents": target_info["agents"], "action": "unchanged"})
420
417
  continue
421
418
  action = "remove" if command == "uninstall" else ("replace" if current is not None else "install")
422
- if link is not None:
423
- action = "remove_link" if command == "uninstall" else "replace_link"
424
- item = {"name": name, "target": key, "old": current, "new": disabled_variant(new) if disabled and new else new}
425
- if link is not None:
426
- item["link"] = link
427
- if disabled and new:
428
- item["disabled"] = True # Uniflow disabled it; the update keeps it disabled.
419
+ item = {"name": name, "target": key, "old": current, "new": new}
429
420
  items.append(item)
430
421
  output.append({"name": name, "target": key, "agents": target_info["agents"], "action": action})
431
422
  if command == "uninstall":
@@ -606,34 +597,11 @@ class Store:
606
597
  "note": "Nome externo ao catálogo: não gerenciado nem removido por este CLI."})
607
598
  continue
608
599
  record = state["installed"].get(name)
609
- link = target / name
610
- from .agents import physical as _physical # local `resolved` nesta função sombreia o helper
611
- # Path(), não a string crua: `physical()` devolve str, e aqui o valor é comparado
612
- # com um Path (`target == store`, que seria SEMPRE falso, matando em silêncio o
613
- # ramo de status external_link) e usado como caminho (`store / name`, TypeError).
614
- store = Path(_physical(uniflow_store(self.home)))
615
- if link.is_symlink() and target == store:
616
- resolved = os.path.realpath(link)
617
- managed = self.managed_target(resolved, name)
618
- rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
619
- "status": "external_link", "revision": None, "activation_verified": False,
620
- "note": "Link no store do Uniflow para " + resolved +
621
- (" (cópia gerenciada pelo crow-skills)" if managed else " (não gerenciado)") +
622
- ". Troque só o link por uma cópia gerenciada: crow-skills install " + name +
623
- " --agents uniflow --global" + ("" if managed else " --replace") + " --yes"})
624
- continue
625
- if (record is None and link.is_symlink() and not (store / name).is_symlink() and (store / name).is_dir()
626
- and os.path.realpath(target) != os.path.realpath(store)
627
- and os.path.realpath(link) == os.path.realpath(store / name)):
628
- rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
629
- "status": "uniflow_link", "revision": None, "activation_verified": False,
630
- "note": "Link projetado pelo Uniflow a partir do seu store; gerenciado em --agents uniflow."})
631
- continue
632
600
  try:
633
601
  entries = snapshot(target / name)
634
602
  status = ("absent" if entries is None else "unmanaged") if record is None else (
635
603
  "missing" if entries is None else "installed" if entries == record["entries"] else
636
- "disabled" if entries == disabled_variant(record["entries"]) else "modified")
604
+ "modified")
637
605
  except CrowError:
638
606
  status = "unsafe"
639
607
  rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
package/crow_skills/ui.py CHANGED
@@ -89,7 +89,7 @@ def word(state):
89
89
 
90
90
  # --- help ----------------------------------------------------------------------------------
91
91
 
92
- TAGLINE = "skills crow nos seus CLIs de agente (Claude Code, Codex, Gemini CLI, Antigravity, Devin, Uniflow)"
92
+ TAGLINE = "skills crow nos seus CLIs de agente (Claude Code, Codex, Gemini CLI, Antigravity, Devin)"
93
93
 
94
94
  SECTIONS = [
95
95
  ("Começar", [
@@ -149,7 +149,7 @@ def flags_help(actions, stream=None):
149
149
  return "\n".join(lines)
150
150
 
151
151
 
152
- SETUP_AGENTS = ("claude", "codex", "gemini", "agy", "devin", "uniflow")
152
+ SETUP_AGENTS = ("claude", "codex", "gemini", "agy", "devin")
153
153
 
154
154
  # One clear sentence comes from the error; the line below gives the exact command to fix it.
155
155
  FIXES = {
@@ -160,7 +160,7 @@ FIXES = {
160
160
  "unmanaged_conflict": "revise o conteúdo; para substituir com backup, acrescente --replace",
161
161
  "unmanaged_link": "acrescente --replace para trocar só o link por uma cópia gerenciada",
162
162
  "guide_requires_skill": "crow-skills setup --yes",
163
- "unknown_agent": "use --agents com: claude, codex, gemini, devin, uniflow (um diretório fora desses: --dest)",
163
+ "unknown_agent": "use --agents com: claude, codex, gemini, devin (um diretório fora desses: --dest)",
164
164
  "unknown_group": "crow-skills groups (lista os grupos; todas = catálogo inteiro)",
165
165
  "source_mismatch": "use install com --source para trocar de origem",
166
166
  "unsupported_platform": "agende crow-skills upgrade --yes por conta própria (ex.: cron)",
@@ -23,7 +23,7 @@ Checklist aplicável: autorização por escopo e flags (sim); validação de nom
23
23
  | Dois instaladores concorrentes | Lock POSIX por perfil; segundo processo real deve retornar busy |
24
24
  | Preview ou list instalando silenciosamente | Sem gravação/lock/fetch no dry-run, flags explícitas em mutações; testes de diretório vazio e fetch proibido |
25
25
  | Guia de agente sobrescrevendo prompt compartilhado ou config alheia | Só mecanismos aditivos verificados; recusa de symlinks, hard links, prompts compartilhados e seus alvos; bloco TOML marcado validado por parse, chave alheia nunca editada; testes de symlink, hard link, chave estrangeira e remoção byte a byte |
26
- | Install/update aplicando o guia de forma parcial ou sobre conteúdo alheio | Guia planejado e conferido antes de gravar skills; recusa falha o comando inteiro sem estado parcial; falha do guia desfaz as skills; transações encadeadas (`linked`) com rollback/recover conjuntos; `--no-guide` explícito; mesmas proteções de Uniflow/symlink/hard link; testes de recusa, falha, interrupção e rollback |
26
+ | Install/update aplicando o guia de forma parcial ou sobre conteúdo alheio | Guia planejado e conferido antes de gravar skills; recusa falha o comando inteiro sem estado parcial; falha do guia desfaz as skills; transações encadeadas (`linked`) com rollback/recover conjuntos; `--no-guide` explícito; proteção de prompts compartilhados e seus alvos, recusa de symlink/hard link e destinos incompatíveis; testes de recusa, falha, interrupção e rollback |
27
27
  | Metadados de complementaridade virando instalação em massa | Relacionamentos opcionais, --with-handoffs direto e explícito, sem expansão de overlays; teste de conjunto exato |
28
28
 
29
29
  Esta tabela define ameaça/controle, não é um certificado de segurança. Resultados executados e revisão independente são registrados separadamente na entrega. Persistem limites de processos hostis do mesmo usuário, filesystem/hardware, confiança no clone/configuração Git escolhido e efeitos que o host venha a permitir ao executar uma skill após instalada.
package/docs/cli.md CHANGED
@@ -22,7 +22,6 @@ crow-skills init --yes # alias
22
22
  | Gemini CLI | `gemini` | `~/.gemini/settings.json` | gemini |
23
23
  | Antigravity | `agy` | `~/.gemini/antigravity-cli` | gemini (mesmo diretório de skills; hooks e status line próprios) |
24
24
  | Devin CLI | `devin` | `~/.config/devin` (ou XDG) | devin |
25
- | Uniflow | `uniflow` | diretório do `config.json` do Uniflow | uniflow (só o store; sem guia nem hooks) |
26
25
  | tmux | `tmux` | `~/.tmux.conf` | segmento `▸▸ crow on`, quando há configuração |
27
26
 
28
27
  Para cada agente encontrado, no escopo do usuário, o mesmo caminho de `install --global`: todas as skills do catálogo (ou `--skills`), o guia, as integrações e os hooks do modo crow, já habilitados/aprovados em cada CLI (`--no-trust` grava os hooks sem aprová-los). Depois, o que vale para a máquina: o modo crow é gravado como `on` só se o usuário nunca escolheu; o autoupdate é ligado (salvo `--no-autoupdate`); broadcast e recursos são só informados (nada é gravado quando valem os padrões, e um `broadcast.json`/`resources.json` do usuário nunca é reescrito).
@@ -154,7 +153,7 @@ crow-skills recover --yes
154
153
  ```
155
154
 
156
155
  - Manifestos/locks/journals ficam sob `~/.local/state/crow-skills/`, separados da descoberta de skills. Não se escreve `manifest.json` dentro de `.claude/skills`.
157
- - Staging e backups ficam no diretório reservado `.crow-skills/` ao lado da raiz de skills, na mesma filesystem para permitir renames, nunca dentro dela. Quando esse lado é protegido (o store do Uniflow), ficam em `~/.local/state/crow-skills/work/`. Backups são privados e retidos; não há expiração automática nesta versão.
156
+ - Staging e backups ficam no diretório reservado `.crow-skills/` ao lado da raiz de skills, na mesma filesystem para permitir renames, nunca dentro dela. Quando esse lado é protegido, ficam no estado privado `~/.local/state/crow-skills/work/`. Backups são privados e retidos; não há expiração automática nesta versão.
158
157
  - Todos os destinos passam por preflight antes de aplicar. Renames são atômicos por pasta; uma operação com múltiplos destinos não é um único rename atômico global. Erros comuns provocam restauração; interrupções deixam journal recuperável, bloqueando novas mutações até `recover`.
159
158
  - `recover` restaura o estado anterior, não tenta terminar silenciosamente a instalação. A própria restauração tem um estado pendente `restoring`, permitindo retomada se também for interrompida. Confere arquivos/backup antes de substituir. Se houver edições após a interrupção, recusa e conserva os dados para resolução manual.
160
159
  - `install`/`update`/`uninstall` que também alteram o guia gravam duas transações encadeadas: skills e depois guia, com o journal do guia apontando a de skills (`linked`). As duas IDs saem no resultado (`transaction` e `guides.transaction`). Se o guia falhar, as skills dessa execução são desfeitas antes do erro. `rollback` da transação de skills desfaz primeiro o guia ligado; `rollback` da transação de guia desfaz só o guia. `recover` de um guia interrompido também desfaz as skills ligadas (`then_rollback` no preview). Uma interrupção exatamente entre as duas deixa as skills aplicadas e o guia `outdated`/`absent`, sem journal pendente: repita o comando ou faça rollback da transação de skills.
@@ -181,9 +180,9 @@ crow-skills skills --check-npm # também confere a última vers
181
180
  | `latest` | instalada e igual à fonte atual deste crow-skills: mesmo conteúdo, mesma revisão e mesma origem registrada. É o único estado em dia. |
182
181
  | `desatualizada` | instalada e íntegra, mas a revisão ou a origem registrada não é a da fonte atual: falta rodar `update`. |
183
182
  | `modificada` | editada no disco; `update` só a substitui com `--overwrite-modified`. |
184
- | `desativada` | instalada e desabilitada (por exemplo, pelo Uniflow). |
183
+ | `desativada` | estado desabilitado legado; não implica ativação pelo host. |
185
184
  | `ausente` | registrada no estado e não encontrada no disco. |
186
- | `nao_gerenciada` | o que está ali não é uma instalação gerenciada por este CLI (pacote de terceiros, link do Uniflow, destino vazio). |
185
+ | `nao_gerenciada` | o que está ali não é uma instalação gerenciada por este CLI (pacote de terceiros, link externo, destino vazio). |
187
186
  | `indeterminada` | não foi possível ler o destino com segurança (`unsafe`). |
188
187
 
189
188
  O rodapé só aparece quando há problema: quantas desatualizadas, modificadas e ausentes, e o comando exato que conserta, `crow-skills update --all --agents <agentes afetados> --global --yes` (com uma linha a mais lembrando que skill editada ou ausente só é substituída acrescentando `--overwrite-modified`). Com `--check-npm`, uma consulta a `npm view @andespindola/crow-skills version` diz se o CLI instalado é a última versão publicada; sem npm no PATH, sem rede ou com erro, o comando informa que não deu para conferir e nunca inventa um número.
@@ -216,47 +215,17 @@ Erros retornam `ok:false` e `error.code/message`. Saídas reais incluem alteraç
216
215
  | claude | `~/.claude/skills` | `.claude/skills` |
217
216
  | gemini | `~/.gemini/skills` | `.gemini/skills` |
218
217
  | devin | `~/.config/devin/skills` (ou XDG_CONFIG_HOME) | `.devin/skills` |
219
- | uniflow | store do Uniflow: `~/.config/uniflow/skills` (veja [Uniflow](#uniflow)) | não existe; recusado com `unsupported_scope` |
220
218
 
221
- Cada adaptador grava no diretório PRÓPRIO do seu CLI: o crow-skills atua em cada CLI diretamente e não mantém pasta compartilhada de skills. Até a 0.14.0 o codex era a exceção, instalando em `~/.agents/skills`, o que fazia o crow-skills agir sobre um diretório em vez de sobre um agente: aparecia um agente `shared` que não é CLI nenhum, e o Codex, que varre as duas raízes, listava a mesma skill duas vezes. O adaptador `shared` foi removido e `~/.agents/skills` virou apenas legado: nada é instalado lá, e o que ficou de versões anteriores é apontado como duplicata para você remover (veja [Codex e a projeção do Uniflow](#codex-e-a-projeção-do-uniflow)). Para gravar num diretório fora dos adaptadores, use `--dest` explicitamente.
219
+ Cada adaptador grava no diretório PRÓPRIO do seu CLI: o crow-skills atua em cada CLI diretamente e não mantém pasta compartilhada de skills. Até a 0.14.0 o codex era a exceção, instalando em `~/.agents/skills`, o que fazia o crow-skills agir sobre um diretório em vez de sobre um agente: aparecia um agente `shared` que não é CLI nenhum, e o Codex, que varre as duas raízes, listava a mesma skill duas vezes. O adaptador `shared` foi removido e `~/.agents/skills` virou apenas legado: nada é instalado lá, e o que ficou de versões anteriores é apontado como duplicata para você remover (veja [pasta compartilhada legada](#codex-e-a-pasta-compartilhada-legada)). Para gravar num diretório fora dos adaptadores, use `--dest` explicitamente.
222
220
 
223
221
  #### Diretório de skills em symlink (dotfiles)
224
222
 
225
- Desde a 0.16.0 o destino de cada adaptador é resolvido para o caminho FÍSICO (`agents.physical`, via `os.path.realpath`): `~/.codex -> /dotfiles/codex` instala em `/dotfiles/codex/skills`, e o symlink é preservado. Vale para os cinco CLIs e também para `--dest`. Antes isso era recusado com `unsafe_destination` em todos eles.
223
+ Os quatro adaptadores resolvem diretórios nativos de dotfiles para o caminho físico antes de gravar: `~/.codex -> /dotfiles/codex` instala em `/dotfiles/codex/skills` e preserva o symlink. Destinos incompatíveis são recusados antes de inspecionar seus alvos; isso não autoriza importar stores de outro host.
226
224
 
227
- O controle de segurança não foi enfraquecido, foi deslocado: `storage.checked()` continua recusando escrever ATRAVÉS de um symlink, e agora valida o destino real, o que é mais preciso. As proteções por LOCALIZAÇÃO continuam valendo depois da resolução: um diretório de skills que resolve para dentro de `~/.config/uniflow` é recusado com `protected_path`, sem tocar a config.
228
-
229
- Duas consequências que vale conhecer:
230
-
231
- - **O arquivo de prompt do agente não é resolvido.** `CLAUDE.md`, `AGENTS.md` e `rules/crow-skills.md` seguem recusados quando o caminho passa por symlink, porque gravar ali reescreveria um arquivo dentro do seu repositório de dotfiles. Isso não derruba mais o comando: o agente aparece em `guides.skipped` com `code: "unsafe_destination"` e as skills são instaladas. Para ter o guia nesse caso, grave-o você mesmo no caminho físico ou use `--no-guide` para não voltar a tentar.
232
- - **Apontar o diretório de um CLI exatamente para o store do Uniflow é RECUSADO numa mutação** (`protected_path`, com a dica de usar `--agents uniflow`). Gravar no store vale para todos os CLIs que o Uniflow projeta, não só para o selecionado, e a resolução tornou esse caminho alcançável a partir da seleção de um CLI. A checagem roda só na mutação: `destinations()` é usado por `status`, `doctor` e pelos avisos do uniflow para inspecionar o destino de outro agente, e recusar ali derrubaria leitura.
233
- - **As integrações (status line, hooks, comando) também passam por `protect()`**, então num diretório de agente em symlink elas são recusadas junto com o guia e aparecem em `integrations.skipped`. Um diretório `.crow-skills` de transações/backup aparece dentro do caminho físico (o repositório de dotfiles) e não é podado depois do commit.
225
+ `storage.checked()` continua recusando escrita através de symlink ou hard link no destino efetivo. Prompts compartilhados e seus alvos físicos permanecem protegidos; guias e integrações são aditivos e só podem alterar seus próprios arquivos ou blocos. Links individuais de pacotes são inseguros: não são seguidos, substituídos ou tratados como instalações ativas. Diretórios de staging e backups pertencem ao crow-skills e permanecem disponíveis para recuperação.
234
226
 
235
227
  `agents` só verifica PATH e caminhos, sem iniciar agente/LLM. `status`/`doctor` conferem arquivos e estado de instalação, não provam carregamento, ativação, independência dos workers ou execução correta da skill. Gemini oferece `/skills reload`; para outros hosts, confira sua lista/reload ou reinicie a sessão conforme a versão. Nenhum system prompt base é alterado; o guia abaixo e os hooks do modo crow, aplicados por padrão em `--global` e desligáveis com `--no-guide` e `--no-integrations`, são aditivos: só informam ao agente o modo escolhido pelo usuário.
236
228
 
237
- ### Uniflow
238
-
239
- ```sh
240
- crow-skills install --all --agents uniflow --global --dry-run
241
- crow-skills install --all --agents uniflow --global --yes
242
- crow-skills status --agents uniflow
243
- crow-skills update --agents uniflow --global --yes
244
- crow-skills uninstall --all --agents uniflow --global --yes
245
- ```
246
-
247
- O Uniflow guarda as skills num store próprio e cria, ao subir cada CLI, links desse store em `~/.claude/skills`, `~/.codex/skills`, `~/.gemini/skills` e `~/.config/devin/skills`; quando o CLI já tem uma entrada nativa com o mesmo nome, ele pula o link. O agente `uniflow` grava nesse store uma cópia real e gerenciada de cada pacote, com o mesmo manifesto e hashes dos outros agentes; `status`, `update`, `uninstall`, `--overwrite-modified`, `rollback` e `recover` funcionam igual.
248
-
249
- - Store: o diretório `skills` ao lado do `config.json` do Uniflow, resolvido como ele resolve: `UNIFLOW_CONFIG` (ou `CROWFLOW_CONFIG`), senão `$XDG_CONFIG_HOME/uniflow`, senão `~/.config/uniflow`. Com `--home` isolado, essas variáveis são ignoradas.
250
- - Proteção: em todo o diretório de configuração do Uniflow, só `skills/crow-*` é gravável. `config.json`, prompts, backups e as outras skills do store continuam recusados com `protected_path`, inclusive por `--dest`. A comparação usa também a identidade do arquivo, então outra grafia (`~/.config/UNIFLOW` no APFS) não fura a proteção. Se `UNIFLOW_CONFIG` aponta para um arquivo direto no home (ou num ancestral dele), só esse arquivo, o store e os marcadores do Uniflow (`.bundled-skills.json`, `skills.required.json`) são protegidos, nunca o home. Staging e backups dessa instalação ficam em `~/.local/state/crow-skills/work/`, não ao lado do store; `rollback`/`recover` de transações antigas procuram os backups nos dois lugares.
251
- - Instalações antigas com `--dest` dentro da configuração do Uniflow (fora de `skills/crow-*`): `uninstall` continua permitido para o que está no manifesto; `install`/`update` são recusados com o comando de remoção e a sugestão de reinstalar com `--agents uniflow --global`.
252
- - Links importados pelo Uniflow: na migração, o Uniflow pode criar `skills/crow-x` como link para a cópia nativa (ex.: `~/.claude/skills/crow-x`). `status`/`doctor` mostram `external_link` (doctor `healthy:false`) com o comando para resolver. `install`/`update` com `--agents uniflow` trocam só o link por uma cópia gerenciada (ação `replace_link`), sem tocar no alvo, e o `rollback` devolve o link. Isso é automático quando o alvo é uma instalação gerenciada pelo crow-skills com arquivos iguais ao manifesto; para outro alvo, o erro `unmanaged_link` explica e pede `--replace`. Links continuam recusados no resto do caminho (store como link, `--dest`).
253
- - Skills desabilitadas no Uniflow (`SKILL.md` renomeado para `SKILL.md.disabled`): `status` mostra `disabled`, o doctor não acusa problema e `update` (mesmo com `--overwrite-modified`) mantém a skill desabilitada.
254
- - Só `--global`: o Uniflow não tem store de projeto (`--project` falha com `unsupported_scope`).
255
- - Sem guia: o Uniflow infere o uso da crow-behaviour por conta própria, então `guide` recusa `uniflow` (`unsupported_guide`) e `install` informa isso em `guides.skipped`. Integrações de interface também não se aplicam.
256
- - Detecção: `agents` marca `detected` quando `uniflow` está no PATH (`executable_found`) ou o diretório de configuração existe (`config_found`).
257
- - Cópias nativas duplicadas: se claude, gemini ou devin já têm um pacote crow de mesmo nome, o Uniflow não cria o link para ele e a cópia nativa continua valendo ali, possivelmente em outra revisão (`effect: skipped`). O codex se comporta igual desde a 0.15.0, por instalar no diretório dele (`effect: skipped`); antes ele instalava na pasta compartilhada e o Codex via a mesma skill duas vezes. `install`/`update` com `uniflow` listam essas cópias em `uniflow.native_duplicates` (diretórios que não puderam ser lidos, como um `~/.claude` symlink de dotfiles, vão para `uniflow.uninspected` sem derrubar o comando) e sugerem em `uniflow.suggested_command` o `uninstall` explícito; nada é removido sem esse comando. Remover a cópia nativa da crow-behaviour remove também o guia daquele agente, que deixa de valer fora do Uniflow.
258
- - Links projetados: em `status`/`doctor`, um link do Uniflow para o store num diretório de agente aparece como `uniflow_link`, e não deixa o doctor `healthy:false`.
259
-
260
229
  #### Estado gravado sob um apelido do destino
261
230
 
262
231
  A chave do estado é o caminho FÍSICO. Um estado escrito quando `~/.claude` ainda era diretório comum fica sob outra chave, e `Store.state()` o encontra varrendo os destinos conhecidos (`aliased_state_path`) para não perder a instalação quando você move tudo para dotfiles. Depois de gravar o estado canônico, a mutação APOSENTA o apelido, renomeando-o para `.superseded.json` em vez de apagar. Sem essa aposentadoria o registro antigo ficava divergente e voltava a mandar caso o diretório deixasse de ser link: uma skill removida ressuscitava como `missing`, com a revisão errada, e o `update` passava a recusar com `source_mismatch`. Dois apelidos para o mesmo caminho físico são ambíguos e nenhum é usado.
@@ -269,26 +238,13 @@ Consequência a conhecer, se você DESFIZER o link depois de ter mexido pelo lin
269
238
 
270
239
  Sem isso, validar um hook rodando-o de verdade registra uma sessão com o `session_id` usado no teste. Essa sessão falsa conta como viva para TODOS os outros agentes da máquina pela janela de `stale_minutes` (30 min por padrão): entra na contagem do bloco, na relevância por repositório e nos avisos de "não troque de branch neste clone". Uma validação dos cinco CLIs chega a inventar dez ou mais sessões `working`.
271
240
 
272
- #### Codex e a projeção do Uniflow
273
-
274
- O Codex lê duas raízes de skills do usuário: `$CODEX_HOME/skills` (padrão `~/.codex/skills`, onde o crow-skills instala e o Uniflow liga o store) e a legada `~/.agents/skills`, onde o crow-skills instalava para o codex até a 0.14.0. Ele segue symlinks e lista duas vezes uma skill de mesmo nome quando as duas entradas apontam para diretórios diferentes — hoje isso só acontece com uma cópia deixada na pasta legada. Instalar no diretório do próprio Codex é seguro ao lado do Uniflow: ele não sobrescreve um nome que já existe ali (registra `skipped`) e só apaga um diretório que tenha o marcador `.uniflow-store.json`, que apenas ele escreve. Por isso:
241
+ #### Codex e a pasta compartilhada legada
275
242
 
276
- - Um link `~/.codex/skills/crow-x` → `<store>/crow-x`, com a entrada do store gerenciada pelo crow-skills, conta como a instalação do codex (`via uniflow`). Entrada desabilitada no Uniflow aparece como `disabled`; entrada escrita à mão, como não gerenciada (com o mesmo motivo e correção do resto do Uniflow). `CODEX_HOME` vale para o perfil real; `--home` isolado usa `<home>/.codex`.
277
- - `setup` e `upgrade` não criam cópia própria para esses nomes e removem, pelo `uninstall` normal contra a pasta legada (`--dest ~/.agents/skills`, com backup, transação e `rollback`), as cópias gerenciadas e intactas que ficaram lá duplicando a projeção (`removida: duplicava a projeção do Uniflow`). Uma cópia editada fica, com aviso e a dica `--overwrite-modified`. Uma cópia também registrada para outro agente daquele diretório fica, porque outro host pode depender dela; o motivo e o comando (`uninstall ... --dest ~/.agents/skills`) aparecem no resumo.
278
- - `install --agents codex` não cria cópia de um nome projetado (`codex_projection.via_uniflow` no resumo) e nunca remove nada: as cópias legadas aparecem em `codex_projection.duplicates`, com `suggested_command`. `--agents shared` não existe mais e é recusado com `unknown_agent`; para gravar num diretório específico, use `--dest`.
279
- - A crow-behaviour continua nativa onde o Uniflow não a projeta, e o guia, as integrações e os hooks do Codex seguem funcionando; quando ela é projetada, o guia conta a projeção.
280
- - Migração da pasta compartilhada antiga: em `setup`, `upgrade` e no autoupdate, depois de instalar no diretório do Codex, o crow-skills remove de `~/.agents/skills` as skills que ELE instalou lá e que estão íntegras, pelo `uninstall` normal (`--dest ~/.agents/skills`, com backup, transação e `rollback`); o resumo do agente traz isso em `legacy_shared`. Isso cobre também uma skill que o Uniflow não projeta (a própria crow-behaviour, tipicamente), que não apareceria como duplicata e ficaria lá para sempre. Uma cópia editada, ou também registrada para outro agente daquele diretório, é mantida e reportada em `kept` com o comando. Nada que não tenha sido instalado pelo crow-skills é tocado, e uma falha na migração nunca derruba o resultado do agente (vira `legacy_shared.state: warn`).
281
- - A cópia legada só sai quando aquele nome REALMENTE funciona no diretório do Codex — instalação íntegra nossa, ou link saudável do Uniflow. Se a entrada do store do Uniflow está quebrada, o link ocupa o nome sem funcionar e a cópia legada é a única skill que o Codex tem: nesse caso ela é mantida e reportada (`legacy_shared_without_replacement`), porque removê-la tiraria a skill em vez de migrá-la. O mesmo vale quando o nome no diretório do Codex é um apelido apontando para a própria cópia legada: remover o alvo deixaria um link pendurado. Cada nome recebe UMA razão em `kept`: uma razão mais específica já registrada para ele (projeção quebrada, destino inseguro) prevalece.
282
- - **Mudanças de comportamento do destino do codex (0.15.0).** Com o destino no diretório próprio do Codex, uma situação que antes não afetava o codex passa a afetá-lo, exatamente como já valia para claude, gemini e devin: (a) a partir da 0.16.0 um `~/.codex`/`$CODEX_HOME` em symlink é RESOLVIDO para o caminho físico (veja a seção de dotfiles acima); na 0.15.0 ele era recusado com `unsafe_destination`; (b) se houver um link de qualquer tipo ocupando o nome no diretório (inclusive o apelido para a pasta legada), aquele nome não pode ser gerenciado: ele é reportado com status `unsafe` e o comando sai com 1, sem alterar o link. Não é uma regra nova do codex, é a mesma regra dos outros CLIs passando a valer para ele.
283
- - **`$CODEX_HOME` relativo é ignorado.** Um valor relativo seria resolvido contra o diretório de trabalho DESTE processo, que não é o do Codex, então o destino dependeria de onde o comando foi rodado e poderia cair dentro de um repositório seu. Só valor absoluto (ou com `~`) é honrado; caso contrário vale `<home>/.codex`.
284
- - **Fallback perdido quando a entrada do store do Uniflow está quebrada.** Até a 0.14.0 o codex tinha duas raízes, então uma entrada quebrada no store ainda deixava uma cópia funcional na pasta compartilhada. Agora o link do Uniflow ocupa o nome no diretório do Codex: a entrada quebrada é reportada (`uniflow_link_modified`, com o comando que conserta o store) e a skill fica sem versão funcional até você consertar. A cópia legada, se existir, NÃO é removida nesse caso — é justamente o que o guarda de substituição protege.
285
- - **Aviso `codex_legacy_shared` em `status`/`doctor`.** Numa máquina sem Uniflow nada do que ficou na pasta compartilhada duplica uma projeção, então o aviso de duplicatas não apareceria e uma instalação feita pela 0.14.0 ficaria invisível, ainda carregada pelo Codex. Esse aviso novo lista essas skills, com o caminho e o comando (`setup --agents codex --yes`, que migra, ou `uninstall … --dest <pasta legada>`).
286
- - **Rollback encadeado.** A migração roda como um `uninstall` próprio logo depois do install e fica atrelada a ele (campo `linked` no journal da transação). Um `rollback` da transação do install desfaz PRIMEIRO a migração, restaurando as cópias legadas, e só então remove a instalação nova: o estado final é o anterior ao comando. Sem esse encadeamento, desfazer só o install deixaria as duas pastas vazias.
287
- - Um link `~/.codex/skills/crow-x` → `~/.agents/skills/crow-x` (apontando para a pasta legada) é só um apelido (o Codex o deduplica pelo caminho canônico): nunca é alterado nem removido.
288
- - `status`/`doctor` mostram `~/.codex/skills` como um destino do codex (`N skills (N via uniflow)`). Duplicatas geram o aviso `codex_duplicates` (`warnings`) com o comando que resolve; o doctor continua `healthy:true`, porque as duas cópias estão íntegras e funcionam (o Codex só as lista duas vezes).
243
+ O destino nativo do Codex é `$CODEX_HOME/skills` (padrão `~/.codex/skills`); `--home` isolado usa `<home>/.codex/skills`. O adaptador `shared` não existe. Nenhuma instalação nova grava em `~/.agents/skills`.
289
244
 
245
+ `setup`, `upgrade` e autoupdate podem migrar cópias legadas gerenciadas e íntegras para o destino nativo, com backup e transação recuperável. A cópia antiga só pode sair depois de verificar uma instalação nativa íntegra como substituta. Cópias editadas, sem substituição válida ou compartilhadas com outro agente ficam preservadas e reportadas. Links individuais de pacotes não contam como instalação nativa saudável e não são seguidos para descobrir outro store.
290
246
 
291
- Fontes primárias consultadas em 2026-10-20: [Codex](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills), [Claude](https://code.claude.com/docs/en/skills), [Gemini](https://geminicli.com/docs/cli/using-agent-skills/), [Devin](https://docs.devin.ai/cli/extensibility/skills/overview).
247
+ Os quatro perfis são independentes: `claude`, `codex`, `gemini` e `devin`. `setup`, `status`, `doctor` e `upgrade` não descobrem nem consultam configuração, ambiente ou stores de hosts removidos. Selecionar um agente removido é erro `unknown_agent`; um destino incompatível é recusado, não importado ou convertido automaticamente. Registros antigos de recuperação incompatíveis ficam preservados e a operação é recusada, sem acessar seus alvos.
292
248
 
293
249
  ## Instruções para o agente (guide)
294
250
 
@@ -306,8 +262,8 @@ crow-skills guide uninstall --agents codex --global --yes
306
262
  Aplicação automática (padrão em `install`, `update` e `uninstall` com `--global`, desligada por `--no-guide`):
307
263
 
308
264
  - O guia é planejado junto com as skills, a partir do conjunto de skills que o agente terá depois do comando, e conferido antes de gravar qualquer coisa. Install/update que deixam a crow-behaviour instalada aplicam o guia ou o regeneram; mudança de outra skill gerenciada regenera um guia já instalado (também quando a mudança chega pelo diretório do codex). Uninstall da crow-behaviour remove o guia, devolvendo o arquivo que `--replace` havia deslocado; uninstall de outra skill regenera o guia instalado e nunca cria um novo.
309
- - Recusa do guia (arquivo não gerenciado sem `--replace`, guia editado/ausente sem `--overwrite-modified`, chave `developer_instructions` alheia, symlink, hard link, prompt compartilhado ou Uniflow) falha o comando inteiro, com código do guia e sugestão de `--replace`, `--overwrite-modified` ou `--no-guide`, sem gravar skills nem guia. Em install/update, `--replace` e `--overwrite-modified` valem para skills e guia.
310
- - `shared`, `uniflow`, `--project` e `--dest` não têm mecanismo: o comando segue e informa em `guides.skipped`. Sem Python 3.11+ (tomllib), o guia do codex também é pulado e informado.
265
+ - Recusa do guia (arquivo não gerenciado sem `--replace`, guia editado/ausente sem `--overwrite-modified`, chave `developer_instructions` alheia, symlink, hard link, prompt compartilhado) falha o comando inteiro, com código do guia e sugestão de `--replace`, `--overwrite-modified` ou `--no-guide`, sem gravar skills nem guia. Em install/update, `--replace` e `--overwrite-modified` valem para skills e guia.
266
+ - `--project` e `--dest` não têm mecanismo: o comando segue e informa em `guides.skipped`. Sem Python 3.11+ (tomllib), o guia do codex também é pulado e informado.
311
267
  - `--dry-run` inclui `guides.changes` e `guides.preview` (conteúdo que seria gravado). A saída JSON traz `guides` com `changes`, `skipped`, `revision` e `transaction`.
312
268
 
313
269
  | Agente | Mecanismo (usuário) | Propriedade |
@@ -319,8 +275,8 @@ Aplicação automática (padrão em `install`, `update` e `uninstall` com `--glo
319
275
 
320
276
  Cada mecanismo foi verificado em 2026-10-21 (Claude Code 2.1.304, Codex CLI 0.163.0, agy 1.4.3, Devin CLI 3000.11.92) com uma sentinela carregada numa sessão nova junto do prompt base já existente: os mecanismos são aditivos. Versões futuras dos CLIs podem mudar isso; confira abrindo uma sessão nova. O comando não inicia agentes e retorna `activation_verified:false`.
321
277
 
322
- - Só `--global` é suportado. No comando `guide`, `--project`, `--dest`, `shared` e `uniflow` são recusados com `unsupported_guide`, porque não há mecanismo verificado.
323
- - Nunca grava em symlinks, em arquivos com hard links, em prompts compartilhados (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.config/devin/CLAUDE.md`), em `~/.config/uniflow/` (só `skills/crow-*` é gravável, e só pela instalação de skills) ou nos alvos desses links. Não usa mecanismos que substituem o prompt base (como `model_instructions_file` ou `AGENTS.override.md`).
278
+ - Só `--global` é suportado. No comando `guide`, `--project`, `--dest` e adaptadores sem mecanismo são recusados com `unsupported_guide`, porque não há mecanismo verificado.
279
+ - Nunca grava em symlinks, em arquivos com hard links, em prompts compartilhados (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.config/devin/CLAUDE.md`) ou nos alvos desses links. Não usa mecanismos que substituem o prompt base (como `model_instructions_file` ou `AGENTS.override.md`).
324
280
  - No Codex, uma chave `developer_instructions` definida fora do bloco é recusada sempre, mesmo com `--replace`. A edição é validada por parse TOML (Python 3.11+): o restante do arquivo precisa continuar idêntico. Uninstall devolve o arquivo byte a byte.
325
281
  - Arquivo existente não gerenciado exige `--replace`; guia editado ou ausente exige `--overwrite-modified`. Ambos guardam backup privado em `~/.local/state/crow-skills/guide-backups/`. O uninstall devolve o arquivo que `--replace` substituiu (conferido pelo SHA-256 do backup), em vez de apagá-lo, e mantém um `config.toml` que já existia, mesmo vazio.
326
282
  - Transações usam o mesmo journal, lock, `rollback` e `recover` das skills. Se o Codex reescrever outras partes do `config.toml` depois, o rollback desfaz só o bloco.
@@ -415,7 +371,7 @@ conservador, e só pede o `doctor` quando há evento fora dele.
415
371
 
416
372
  Regras de preservação:
417
373
 
418
- - Antes de editar, o arquivo original vai para `~/.local/state/crow-skills/integrations/backups/` (privado). Gravação atômica, nunca através de symlink ou hard link; prompts compartilhados e `~/.config/uniflow` são recusados como no guia.
374
+ - Antes de editar, o arquivo original vai para `~/.local/state/crow-skills/integrations/backups/` (privado). Gravação atômica, nunca através de symlink ou hard link; prompts compartilhados e seus alvos físicos são recusados como no guia.
419
375
  - Claude: um `statusLine` existente do tipo `command` é encadeado: o comando anterior roda primeiro, com o mesmo stdin, sua saída é mantida inteira (todas as linhas, espaços internos) e o segmento crow entra no fim da última linha; as demais chaves dele (`padding` etc.) são preservadas. Outro tipo é mantido e a integração é pulada. Se você mudar o `statusLine` depois, ele não é sobrescrito nem removido. No uninstall, se `settings.json` ainda tiver exatamente o que o crow-skills gravou, os bytes originais do backup voltam; se você editou outra parte do arquivo, a restauração é só da chave `statusLine` (JSON reescrito com indentação 2).
420
376
  - Codex: só aplicado se você ainda não definiu `tui.status_line`; um `[tui]` existente recebe o bloco logo após o cabeçalho, senão o bloco cria a tabela no fim do arquivo. A edição é verificada por parse TOML (Python 3.11+). O uninstall só remove o bloco se ele estiver como foi gravado e se o resultado tiver as mesmas chaves que o arquivo atual menos `tui.status_line`; sem outras edições, o arquivo volta byte a byte. Chaves acrescentadas a um `[tui]` criado pelo crow-skills mantêm o cabeçalho `[tui]`; bloco editado ou remoção que mudaria outra chave é recusado e reportado em `skipped`. O Codex não aceita comando externo na status line, então ela não lê o ledger.
421
377
  - tmux: só quando o `tmux` está no PATH e já existe `~/.tmux.conf` ou `~/.config/tmux/tmux.conf`. Se o `tmux.conf` for symlink, o snippet é instalado mas o arquivo não é editado: o resultado traz a linha `source-file -q ...` para você adicionar. O snippet acrescenta o segmento só quando `status-right` ainda não o contém, então ele volta depois de cada `tmux source-file` do seu `tmux.conf` sem duplicar. O diretório do painel chega ao script escapado por `#{q:pane_current_path}` (tmux 3.0 ou mais novo): nome de diretório com `$(...)` ou crase não é executado. Depois de remover, o segmento sai da sessão atual quando o `status-right` é redefinido (recarregar um `tmux.conf` que o define) ou quando o servidor tmux reinicia.
@@ -619,7 +575,7 @@ Cada execução:
619
575
 
620
576
  1. Atualiza o próprio crow-skills conforme a instalação: npm global (`npm install -g @andespindola/crow-skills@latest`), que é a fonte desde a 0.16.1. A detecção de Homebrew (`brew upgrade andersonflima/tap/crow-skills`) continua no código para quem ainda tem uma instalação antiga, mas o tap está congelado na 0.16.1 e não recebe versões novas: um `upgrade` numa instalação brew vai dizer "já na versão mais nova" mesmo havendo versão nova no npm. Checkout Git só com `autoupdate on --git-pull`, e só se estiver limpo em `main`/`master` (`git pull --ff-only`); caso contrário, é pulado com o motivo. Saída de brew/npm/git não é guardada nem impressa, só código de saída e versões.
621
577
  2. Se a versão mudou, a versão nova (o launcher) faz o passo seguinte, para usar o snapshot novo como fonte.
622
- 3. Para cada agente com skills gerenciadas (claude, codex/`$CODEX_HOME/skills`, gemini, devin, uniflow), roda o equivalente a `update <skills> --agents <agente> --global --yes`: regenera guia, integrações e hooks; a [identidade do pacote](#origem-e-versões) evita `source_mismatch` depois do upgrade. Skills editadas localmente (`modified`), ausentes ou instaladas de outra origem (`other_origin`, por exemplo de um clone Git) ficam como estão e aparecem em `kept`; nada é sobrescrito. Uma reaplicação dos hooks troca as entradas do crow-skills no lugar, sem mudar a posição das suas. Uma recusa num agente (por exemplo, guia editado) aparece em `error` e não interrompe os outros.
578
+ 3. Para cada agente com skills gerenciadas (claude, codex/`$CODEX_HOME/skills`, gemini, devin), roda o equivalente a `update <skills> --agents <agente> --global --yes`: regenera guia, integrações e hooks; a [identidade do pacote](#origem-e-versões) evita `source_mismatch` depois do upgrade. Skills editadas localmente (`modified`), ausentes ou instaladas de outra origem (`other_origin`, por exemplo de um clone Git) ficam como estão e aparecem em `kept`; nada é sobrescrito. Uma reaplicação dos hooks troca as entradas do crow-skills no lugar, sem mudar a posição das suas. Uma recusa num agente (por exemplo, guia editado) aparece em `error` e não interrompe os outros.
623
579
 
624
580
  O resultado fica em `~/.local/state/crow-skills/autoupdate/state.json` (`lastRun` e as últimas 20 execuções), aparece em `autoupdate status`, em `status`/`doctor` (doctor `healthy:false` se a última execução falhou ou o agendador foi alterado/removido) e em `events snapshot`. Duas execuções simultâneas são recusadas (`busy`).
625
581
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/crow-skills",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Versioned, complementary agent skills with a protected multi-agent installer",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {