@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.
- package/README.md +3 -3
- package/catalog.json +15 -0
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/agents.py +35 -70
- package/crow_skills/autoupdate.py +4 -96
- package/crow_skills/broadcast.py +9 -7
- package/crow_skills/cli.py +12 -157
- package/crow_skills/guide.py +24 -64
- package/crow_skills/machine.py +32 -194
- package/crow_skills/projection.py +3 -148
- package/crow_skills/storage.py +54 -86
- package/crow_skills/ui.py +3 -3
- package/docs/cli-security.md +1 -1
- package/docs/cli.md +16 -60
- package/package.json +1 -1
- package/release.json +25 -20
- package/skills/crow-behaviour/SKILL.md +3 -1
- package/skills/crow-behaviour/references/broadcast.md +7 -3
- package/skills/crow-behaviour/references/cross-session-capacity.md +12 -0
- package/skills/crow-ml/SKILL.md +35 -0
package/crow_skills/storage.py
CHANGED
|
@@ -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
|
-
"""
|
|
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)
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
388
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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"
|
|
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
|
|
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)",
|
package/docs/cli-security.md
CHANGED
|
@@ -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;
|
|
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
|
|
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` |
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
310
|
-
-
|
|
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
|
|
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`)
|
|
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
|
|
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
|
|
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