@andespindola/crow-skills 0.6.1 → 0.7.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/crow_skills/__init__.py +1 -1
- package/crow_skills/agents.py +55 -5
- package/crow_skills/cli.py +53 -2
- package/crow_skills/guide.py +51 -10
- package/crow_skills/storage.py +140 -14
- package/docs/cli.md +30 -6
- package/package.json +1 -1
- package/release.json +10 -10
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Skills com escopo explícito, conhecimento especializado preservado e fluxos comuns reutilizáveis.
|
|
4
4
|
|
|
5
|
-
**Versão 0.
|
|
5
|
+
**Versão 0.7.0** ([mudanças](CHANGELOG.md)): o Uniflow vira agente de instalação (`--agents uniflow`): as skills crow vão para o store de skills dele, que ele projeta nos CLIs que sobe. A 0.6.1 passou a mostrar o progresso da crow-behaviour na própria conversa; a 0.6.0 trouxe as regras de entrega das 32 skills especialistas, a partir da [validação no Codex](docs/evaluations/2026-10-26-codex.md).
|
|
6
6
|
|
|
7
7
|
**Reconstrução em andamento.** A primeira versão com 32 contratos genéricos não atende à preservação de capacidades solicitada. Não use esse número nem o mapeamento por categoria como prova de substituição das originais. A revisão passa a abranger cada definição e seus recursos, modos e dependências, sem número fixo de skills ou limite artificial de linhas. Veja [critérios e progresso](docs/reconstruction.md).
|
|
8
8
|
|
|
@@ -27,10 +27,10 @@ Exemplos:
|
|
|
27
27
|
|
|
28
28
|
## Instalação e portabilidade
|
|
29
29
|
|
|
30
|
-
O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm privada `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente. Em `--global`, instalar a crow-behaviour também grava o guia aditivo do agente (claude, codex, gemini, devin) para usar as skills; `--no-guide` desliga. As integrações de interface (status line do Claude Code e do Codex, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga.
|
|
30
|
+
O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm privada `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente. No [Uniflow](docs/cli.md#uniflow), `--agents uniflow --global` grava as skills no store dele (`~/.config/uniflow/skills`), sem tocar no resto da configuração. Em `--global`, instalar a crow-behaviour também grava o guia aditivo do agente (claude, codex, gemini, devin) para usar as skills; `--no-guide` desliga. As integrações de interface (status line do Claude Code e do Codex, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga.
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
npm install -g @andespindola/crow-skills@0.
|
|
33
|
+
npm install -g @andespindola/crow-skills@0.7.0
|
|
34
34
|
crow-skills progress
|
|
35
35
|
./bin/crow-skills list
|
|
36
36
|
./bin/crow-skills install crow-behaviour --agents claude,codex --global --dry-run
|
package/crow_skills/__init__.py
CHANGED
package/crow_skills/agents.py
CHANGED
|
@@ -10,9 +10,49 @@ AGENTS = {
|
|
|
10
10
|
"gemini": (".gemini/skills", ".gemini/skills", "gemini"),
|
|
11
11
|
"devin": (".config/devin/skills", ".devin/skills", "devin"),
|
|
12
12
|
"shared": (".agents/skills", ".agents/skills", None),
|
|
13
|
+
# Uniflow keeps one store and links it into each CLI's user skills dir; it has
|
|
14
|
+
# no project scope. The store sits beside config.json (see uniflow_store).
|
|
15
|
+
"uniflow": (".config/uniflow/skills", None, "uniflow"),
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
def real_home(home):
|
|
20
|
+
"""Environment overrides describe the real profile; an isolated --home ignores them."""
|
|
21
|
+
return home == Path.home().resolve()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def uniflow_config_file(home):
|
|
25
|
+
"""Same resolution as Uniflow: UNIFLOW_CONFIG/CROWFLOW_CONFIG name config.json,
|
|
26
|
+
otherwise $XDG_CONFIG_HOME/uniflow/config.json, otherwise ~/.config/uniflow/config.json."""
|
|
27
|
+
if real_home(home):
|
|
28
|
+
override = os.getenv("UNIFLOW_CONFIG") or os.getenv("CROWFLOW_CONFIG")
|
|
29
|
+
if override:
|
|
30
|
+
return Path(os.path.abspath(Path(override).expanduser()))
|
|
31
|
+
if os.getenv("XDG_CONFIG_HOME"):
|
|
32
|
+
return Path(os.path.abspath(Path(os.environ["XDG_CONFIG_HOME"]).expanduser() / "uniflow/config.json"))
|
|
33
|
+
return Path(os.path.abspath(home / ".config/uniflow/config.json"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def uniflow_config_dir(home):
|
|
37
|
+
return uniflow_config_file(home).parent
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def uniflow_guards(home):
|
|
41
|
+
"""Uniflow's own paths to protect: its whole config dir, unless that dir is the home
|
|
42
|
+
or one of its ancestors (UNIFLOW_CONFIG=~/uniflow.json). Then only Uniflow's files
|
|
43
|
+
and store are protected, never the home itself."""
|
|
44
|
+
config = uniflow_config_dir(home)
|
|
45
|
+
profile = Path(os.path.abspath(home))
|
|
46
|
+
if profile == config or profile.is_relative_to(config):
|
|
47
|
+
return [uniflow_config_file(home), config / "skills", config / ".bundled-skills.json",
|
|
48
|
+
config / "skills.required.json"]
|
|
49
|
+
return [config]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def uniflow_store(home):
|
|
53
|
+
return uniflow_config_dir(home) / "skills"
|
|
54
|
+
|
|
55
|
+
|
|
16
56
|
def destinations(names, home, project=None, custom=None):
|
|
17
57
|
if custom:
|
|
18
58
|
if names:
|
|
@@ -27,10 +67,15 @@ def destinations(names, home, project=None, custom=None):
|
|
|
27
67
|
if name not in AGENTS:
|
|
28
68
|
raise CrowError("unknown_agent", "Agente desconhecido: " + name)
|
|
29
69
|
user_path, project_path, _ = AGENTS[name]
|
|
70
|
+
if project and project_path is None:
|
|
71
|
+
raise CrowError("unsupported_scope", "O Uniflow só tem o store global (" + user_path + ", ao lado do config.json); "
|
|
72
|
+
"use --global. Ele projeta esse store nos agentes por conta própria.")
|
|
30
73
|
path = (Path(project) / project_path) if project else (home / user_path)
|
|
31
74
|
# Respect explicit XDG config, but isolate it when --home selects another profile.
|
|
32
|
-
if name == "devin" and not project and home
|
|
75
|
+
if name == "devin" and not project and real_home(home) and os.getenv("XDG_CONFIG_HOME"):
|
|
33
76
|
path = Path(os.environ["XDG_CONFIG_HOME"]).expanduser() / "devin/skills"
|
|
77
|
+
if name == "uniflow":
|
|
78
|
+
path = uniflow_store(home)
|
|
34
79
|
key = str(path.absolute())
|
|
35
80
|
result.setdefault(key, {"path": key, "agents": []})
|
|
36
81
|
if name not in result[key]["agents"]:
|
|
@@ -39,7 +84,12 @@ def destinations(names, home, project=None, custom=None):
|
|
|
39
84
|
|
|
40
85
|
|
|
41
86
|
def inventory(home):
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
87
|
+
rows = []
|
|
88
|
+
for name, (_, project, binary) in AGENTS.items():
|
|
89
|
+
path = Path(destinations(name, home)[0]["path"])
|
|
90
|
+
found = bool(binary and shutil.which(binary))
|
|
91
|
+
# Detection is PATH or the agent's config dir; neither proves the skills load.
|
|
92
|
+
rows.append({"name": name, "executable_found": found, "config_found": path.parent.is_dir(),
|
|
93
|
+
"detected": found or path.parent.is_dir(), "global_path": str(path),
|
|
94
|
+
"project_path": project, "activation_verified": False})
|
|
95
|
+
return rows
|
package/crow_skills/cli.py
CHANGED
|
@@ -248,7 +248,13 @@ def behaviour_agents(home, store, plan=None):
|
|
|
248
248
|
for agent in guide.MECHANISMS:
|
|
249
249
|
path = os.path.abspath(destinations(agent, home)[0]["path"])
|
|
250
250
|
added, removed = touched.get(path, (set(), set()))
|
|
251
|
-
|
|
251
|
+
try:
|
|
252
|
+
managed = set(guide.managed_skills(agent, home, store))
|
|
253
|
+
except CrowError:
|
|
254
|
+
# An unreadable skills root (e.g. a dotfiles symlink) holds nothing this CLI manages;
|
|
255
|
+
# it must not fail a command aimed at another agent.
|
|
256
|
+
managed = set()
|
|
257
|
+
if "crow-behaviour" in (managed | added) - removed:
|
|
252
258
|
result.append(agent)
|
|
253
259
|
return result
|
|
254
260
|
|
|
@@ -321,6 +327,48 @@ def linked_guides(store, txid):
|
|
|
321
327
|
return [t["id"] for t in store.transactions() if t.get("kind") == "guide" and t.get("linked") == txid and t["phase"] == "committed"]
|
|
322
328
|
|
|
323
329
|
|
|
330
|
+
# What a native copy means once Uniflow links its store into each CLI's user skills dir.
|
|
331
|
+
# Uniflow links codex into ~/.codex/skills, not into ~/.agents/skills (this CLI's codex root).
|
|
332
|
+
NATIVE_EFFECT = {
|
|
333
|
+
"claude": "skipped", "gemini": "skipped", "devin": "skipped",
|
|
334
|
+
"codex": "both",
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def uniflow_notice(home, store, targets, plan):
|
|
339
|
+
"""Installing into the Uniflow store while native crow-* copies remain. Uniflow does
|
|
340
|
+
not link a name a CLI already has in the same dir, so that copy (possibly another
|
|
341
|
+
revision) wins there; codex reads ~/.agents/skills and Uniflow's ~/.codex/skills
|
|
342
|
+
link, so it sees both. Report it and the explicit command; never remove anything."""
|
|
343
|
+
if not any("uniflow" in t["agents"] for t in targets):
|
|
344
|
+
return None
|
|
345
|
+
names = sorted({c["name"] for c in plan["changes"] if "uniflow" in c["agents"]})
|
|
346
|
+
native, uninspected = {}, []
|
|
347
|
+
for agent in ("claude", "codex", "gemini", "devin"):
|
|
348
|
+
target = destinations(agent, home)[0]
|
|
349
|
+
try:
|
|
350
|
+
rows = store.status([target], names)
|
|
351
|
+
except (CrowError, OSError) as exc:
|
|
352
|
+
# Informative only: an unreadable native dir (e.g. a dotfiles symlink) never blocks the install.
|
|
353
|
+
uninspected.append({"agent": agent, "target": target["path"], "reason": str(exc) or "falha de leitura"})
|
|
354
|
+
continue
|
|
355
|
+
for row in rows:
|
|
356
|
+
if row["name"] in names and row["status"] in ("installed", "modified", "unmanaged"):
|
|
357
|
+
native.setdefault(row["target"], {"target": row["target"], "agents": target["agents"],
|
|
358
|
+
"effect": NATIVE_EFFECT[agent], "skills": []})
|
|
359
|
+
native[row["target"]]["skills"].append({"name": row["name"], "status": row["status"]})
|
|
360
|
+
if not native:
|
|
361
|
+
return {"native_duplicates": [], "uninspected": uninspected}
|
|
362
|
+
agents = sorted({a for n in native.values() for a in n["agents"]})
|
|
363
|
+
managed = sorted({s["name"] for n in native.values() for s in n["skills"] if s["status"] != "unmanaged"})
|
|
364
|
+
return {"native_duplicates": list(native.values()), "uninspected": uninspected,
|
|
365
|
+
"note": "effect=skipped: o Uniflow não cria o link de um nome que o agente já tem nativo, e essa cópia vale no lugar do "
|
|
366
|
+
"store. effect=both: o Uniflow liga o store em ~/.codex/skills e o Codex também lê ~/.agents/skills, então vê as "
|
|
367
|
+
"duas cópias. Nada foi removido. Remover a cópia nativa de crow-behaviour remove também o guia daquele agente.",
|
|
368
|
+
"suggested_command": ("crow-skills uninstall " + " ".join(managed) + " --agents " + ",".join(agents) + " --global")
|
|
369
|
+
if managed else None}
|
|
370
|
+
|
|
371
|
+
|
|
324
372
|
def run(args):
|
|
325
373
|
if args.version:
|
|
326
374
|
return {"version": __version__}
|
|
@@ -353,7 +401,7 @@ def run(args):
|
|
|
353
401
|
result = {"installations": rows, "guides": guides, "integrations": ui, "pending_transactions": pending,
|
|
354
402
|
"activation_verified": False, "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
|
|
355
403
|
if args.command == "doctor":
|
|
356
|
-
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows + guides),
|
|
404
|
+
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides),
|
|
357
405
|
agents=inventory(home))
|
|
358
406
|
return result
|
|
359
407
|
if args.command in ("rollback", "recover"):
|
|
@@ -429,6 +477,9 @@ def run(args):
|
|
|
429
477
|
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
430
478
|
"relationships": {n: source.related(n) for n in names} if source else {},
|
|
431
479
|
"guides": guides, "integrations": ui, "handoffs_are_optional": True, "activation_verified": False}
|
|
480
|
+
notice = uniflow_notice(home, store, targets, plan) if args.command != "uninstall" else None
|
|
481
|
+
if notice is not None:
|
|
482
|
+
summary["uniflow"] = notice
|
|
432
483
|
if plan["items"] or (gplan and gplan["items"]) or ui.get("changes"):
|
|
433
484
|
authorize(args, interactive, summary)
|
|
434
485
|
txid = None
|
package/crow_skills/guide.py
CHANGED
|
@@ -24,8 +24,8 @@ try: # Python 3.11+; required only to verify config.toml edits.
|
|
|
24
24
|
except ImportError: # pragma: no cover - CI and supported hosts have 3.11+
|
|
25
25
|
tomllib = None
|
|
26
26
|
|
|
27
|
-
from .agents import destinations
|
|
28
|
-
from .source import CrowError, SHA
|
|
27
|
+
from .agents import destinations, uniflow_guards, uniflow_store
|
|
28
|
+
from .source import NAME, CrowError, SHA
|
|
29
29
|
from .storage import atomic_json, checked, private_dir, read_json
|
|
30
30
|
|
|
31
31
|
HEADER = "<!-- Gerenciado por crow-skills guide. Não edite; use crow-skills guide install/uninstall. -->"
|
|
@@ -53,6 +53,7 @@ MECHANISMS = {
|
|
|
53
53
|
}
|
|
54
54
|
UNVERIFIED = {
|
|
55
55
|
"shared": "shared é um diretório de skills, não um CLI; não há onde gravar instruções.",
|
|
56
|
+
"uniflow": "o Uniflow infere o uso de crow-behaviour por padrão e já projeta o store nos agentes; não há guia a gravar.",
|
|
56
57
|
}
|
|
57
58
|
# Shared prompt files (often symlinks into ~/.config/uniflow) and their targets.
|
|
58
59
|
PROTECTED = (".claude/CLAUDE.md", ".codex/AGENTS.md", ".codex/AGENTS.override.md", ".gemini/GEMINI.md",
|
|
@@ -87,17 +88,57 @@ def select(agents):
|
|
|
87
88
|
return result
|
|
88
89
|
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
UNIFLOW_SKILLS = "Somente ~/.config/uniflow/skills/crow-* (o store de skills do Uniflow) é gravável: "
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def inside(path, guard):
|
|
95
|
+
"""path is guard or below it: textually, or by file identity for an existing guard,
|
|
96
|
+
so a case-insensitive filesystem (APFS) or another spelling does not bypass it."""
|
|
97
|
+
if path == guard or path.is_relative_to(guard):
|
|
98
|
+
return True
|
|
99
|
+
if not os.path.exists(guard):
|
|
100
|
+
return False
|
|
101
|
+
for part in (path, *path.parents):
|
|
102
|
+
try:
|
|
103
|
+
if os.path.exists(part) and os.path.samefile(part, guard):
|
|
104
|
+
return True
|
|
105
|
+
except OSError:
|
|
106
|
+
continue
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def shielded(path, home):
|
|
111
|
+
"""True when path is a shared prompt, Uniflow's config (as Uniflow resolves it) or inside their targets."""
|
|
112
|
+
path = Path(os.path.abspath(path))
|
|
113
|
+
for guard in [Path(os.path.abspath(home / rel)) for rel in PROTECTED] + uniflow_guards(home):
|
|
95
114
|
guards = {guard}
|
|
96
115
|
if os.path.lexists(guard):
|
|
97
116
|
guards.add(Path(os.path.realpath(guard)))
|
|
98
|
-
for item in guards:
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
if any(inside(path, item) for item in guards):
|
|
118
|
+
return True
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def protect(path, home):
|
|
123
|
+
"""Refuse symlinked components, shared prompts and anything inside their targets."""
|
|
124
|
+
path = checked(path)
|
|
125
|
+
if shielded(path, home):
|
|
126
|
+
raise CrowError("protected_path", "Arquivo de prompt compartilhado (ou alvo dele) não é gravado: " + str(path))
|
|
127
|
+
return path
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def protect_skill(path, home):
|
|
131
|
+
"""Skill packages follow protect(), with one exception: a crow-* package directly
|
|
132
|
+
inside the Uniflow store. The store's other entries and the rest of the Uniflow
|
|
133
|
+
config (config.json, prompts, backups) stay protected. The package entry itself may
|
|
134
|
+
be a link Uniflow made; the plan decides about it, never following it."""
|
|
135
|
+
path = Path(os.path.abspath(path))
|
|
136
|
+
if path.parent == uniflow_store(home) and NAME.fullmatch(path.name):
|
|
137
|
+
checked(path.parent)
|
|
138
|
+
return path
|
|
139
|
+
path = checked(path)
|
|
140
|
+
if shielded(path, home):
|
|
141
|
+
raise CrowError("protected_path", UNIFLOW_SKILLS + str(path) + " é protegido.")
|
|
101
142
|
return path
|
|
102
143
|
|
|
103
144
|
|
package/crow_skills/storage.py
CHANGED
|
@@ -15,6 +15,7 @@ import shutil
|
|
|
15
15
|
import stat
|
|
16
16
|
import uuid
|
|
17
17
|
|
|
18
|
+
from .agents import uniflow_store
|
|
18
19
|
from .source import CrowError, MAX_FILE, MAX_PACKAGE, MAX_FILES, SHA, digest, file_entry, safe_relative, valid_name
|
|
19
20
|
|
|
20
21
|
|
|
@@ -100,6 +101,22 @@ def read_json(path):
|
|
|
100
101
|
raise CrowError("invalid_state", "Estado/registro transacional inválido; nenhuma alteração aplicada.") from exc
|
|
101
102
|
|
|
102
103
|
|
|
104
|
+
DISABLED = "SKILL.md.disabled"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def disabled_variant(entries):
|
|
108
|
+
"""The same package as Uniflow leaves it after /skills disable (SKILL.md renamed)."""
|
|
109
|
+
if not entries or "SKILL.md" not in entries:
|
|
110
|
+
return None
|
|
111
|
+
result = dict(entries)
|
|
112
|
+
result[DISABLED] = result.pop("SKILL.md")
|
|
113
|
+
return dict(sorted(result.items()))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def same_link(path, value):
|
|
117
|
+
return os.path.islink(path) and os.readlink(path) == value
|
|
118
|
+
|
|
119
|
+
|
|
103
120
|
def validate_entries(entries):
|
|
104
121
|
if not isinstance(entries, dict) or len(entries) > MAX_FILES * 2:
|
|
105
122
|
raise CrowError("invalid_state", "Inventário inválido no estado.")
|
|
@@ -172,6 +189,8 @@ class Store:
|
|
|
172
189
|
if item["target"] not in targets:
|
|
173
190
|
raise ValueError("item target")
|
|
174
191
|
valid_name(item["name"])
|
|
192
|
+
if not isinstance(item.get("link", ""), str) or not isinstance(item.get("disabled", False), bool):
|
|
193
|
+
raise ValueError("item link/disabled")
|
|
175
194
|
for key in ("old", "new"):
|
|
176
195
|
if item[key] is not None:
|
|
177
196
|
validate_entries(item[key])
|
|
@@ -200,11 +219,42 @@ class Store:
|
|
|
200
219
|
finally:
|
|
201
220
|
os.close(fd)
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
def work(self, target, txid):
|
|
223
|
+
from .guide import shielded
|
|
224
|
+
target = checked(target)
|
|
225
|
+
key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
|
|
226
|
+
# Beside the discovery dir, unless that parent is protected (the Uniflow
|
|
227
|
+
# config): then staging/backups stay in the private state root instead.
|
|
228
|
+
base = self.root / "work" if shielded(target.parent / ".crow-skills", self.home) else target.parent / ".crow-skills"
|
|
229
|
+
return checked(base / key / txid)
|
|
230
|
+
|
|
231
|
+
def existing_work(self, target, txid):
|
|
232
|
+
"""Backups of a transaction, wherever the version that made it put them: older
|
|
233
|
+
transactions kept them beside the target even where that is now protected."""
|
|
234
|
+
primary = self.work(target, txid)
|
|
205
235
|
target = checked(target)
|
|
206
236
|
key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
|
|
207
|
-
|
|
237
|
+
for candidate in (primary, target.parent / ".crow-skills" / key / txid, self.root / "work" / key / txid):
|
|
238
|
+
if os.path.lexists(candidate):
|
|
239
|
+
return checked(candidate)
|
|
240
|
+
return primary
|
|
241
|
+
|
|
242
|
+
def managed_target(self, resolved, name):
|
|
243
|
+
"""resolved is a package this CLI manages somewhere and its files match the manifest."""
|
|
244
|
+
directory = self.root / "targets"
|
|
245
|
+
if not directory.is_dir():
|
|
246
|
+
return False
|
|
247
|
+
for state_file in directory.glob("*/state.json"):
|
|
248
|
+
try:
|
|
249
|
+
data = read_json(state_file)
|
|
250
|
+
validate_state(data, data["target"])
|
|
251
|
+
record = data["installed"].get(name)
|
|
252
|
+
place = Path(data["target"]) / name
|
|
253
|
+
if record and os.path.realpath(place) == resolved and snapshot(place) == record["entries"]:
|
|
254
|
+
return True
|
|
255
|
+
except (CrowError, KeyError, TypeError, OSError):
|
|
256
|
+
continue
|
|
257
|
+
return False
|
|
208
258
|
|
|
209
259
|
def plan(self, command, names, targets, source=None, replace=False, overwrite=False):
|
|
210
260
|
if self.pending():
|
|
@@ -220,16 +270,45 @@ class Store:
|
|
|
220
270
|
raise CrowError("unsafe_destination", "Não instale sobre o próprio repositório fonte.")
|
|
221
271
|
state = self.state(target)
|
|
222
272
|
key = str(target)
|
|
273
|
+
from .guide import protect_skill
|
|
274
|
+
for name in (names or state["installed"]):
|
|
275
|
+
try:
|
|
276
|
+
protect_skill(target / valid_name(name), self.home)
|
|
277
|
+
except CrowError as exc:
|
|
278
|
+
if exc.code != "protected_path" or name not in state["installed"]:
|
|
279
|
+
raise
|
|
280
|
+
if command != "uninstall":
|
|
281
|
+
raise CrowError("protected_path", str(exc) + " Instalação anterior a esta proteção: remova com "
|
|
282
|
+
"crow-skills uninstall " + name + " --dest " + str(target) + " --yes e, se for para o "
|
|
283
|
+
"Uniflow, reinstale com --agents uniflow --global.") from exc
|
|
284
|
+
# Only the uniflow agent replaces Uniflow's import links; --dest keeps refusing links.
|
|
285
|
+
store_target = "uniflow" in target_info["agents"] and target == uniflow_store(self.home)
|
|
223
286
|
before[key] = state
|
|
224
287
|
after[key] = json.loads(json.dumps(state))
|
|
225
288
|
selected = list(names) if names else list(state["installed"])
|
|
226
289
|
for name in selected:
|
|
227
290
|
valid_name(name)
|
|
228
291
|
record = state["installed"].get(name)
|
|
229
|
-
|
|
292
|
+
dest = target / name
|
|
293
|
+
link, managed_link = None, False
|
|
294
|
+
if store_target and dest.is_symlink():
|
|
295
|
+
# Uniflow imports a native copy as a link store/crow-x -> its realpath.
|
|
296
|
+
link = os.readlink(dest)
|
|
297
|
+
resolved = os.path.realpath(dest)
|
|
298
|
+
managed_link = self.managed_target(resolved, name)
|
|
299
|
+
if not (managed_link or replace or overwrite):
|
|
300
|
+
raise CrowError("unmanaged_link", str(dest) + " é um link (do Uniflow) para " + resolved +
|
|
301
|
+
", que não é uma instalação gerenciada pelo crow-skills. Para trocar só o link por uma "
|
|
302
|
+
"cópia gerenciada, sem tocar no alvo: crow-skills install " + name +
|
|
303
|
+
" --agents uniflow --global --replace --yes. Ou remova o link você mesmo.")
|
|
304
|
+
current = None
|
|
305
|
+
else:
|
|
306
|
+
current = snapshot(dest)
|
|
307
|
+
disabled = bool(record and current is not None and current == disabled_variant(record["entries"]))
|
|
308
|
+
effective = record["entries"] if disabled else current
|
|
230
309
|
if command != "install" and record is None:
|
|
231
310
|
raise CrowError("not_managed", "Skill não gerenciada neste destino: " + name)
|
|
232
|
-
if record and
|
|
311
|
+
if record and effective != record["entries"] and not overwrite and not managed_link:
|
|
233
312
|
raise CrowError("local_changes", "Skill modificada/ausente: " + name + ". Revise; --overwrite-modified autoriza preservar backup e substituir/remover.")
|
|
234
313
|
if not record and current is not None and not replace:
|
|
235
314
|
raise CrowError("unmanaged_conflict", "Destino já existe e não é gerenciado: " + name + ". Use --replace após revisar.")
|
|
@@ -240,12 +319,19 @@ class Store:
|
|
|
240
319
|
if name not in packages:
|
|
241
320
|
packages[name] = source.package(name)
|
|
242
321
|
new = packages[name].entries
|
|
243
|
-
if (record and command != "uninstall" and
|
|
322
|
+
if (record and command != "uninstall" and link is None and effective == new
|
|
244
323
|
and record["revision"] == source.revision and record.get("source") == str(source.path)):
|
|
245
324
|
output.append({"name": name, "target": key, "agents": target_info["agents"], "action": "unchanged"})
|
|
246
325
|
continue
|
|
247
326
|
action = "remove" if command == "uninstall" else ("replace" if current is not None else "install")
|
|
248
|
-
|
|
327
|
+
if link is not None:
|
|
328
|
+
action = "remove_link" if command == "uninstall" else "replace_link"
|
|
329
|
+
item = {"name": name, "target": key, "old": current, "new": disabled_variant(new) if disabled and new else new}
|
|
330
|
+
if link is not None:
|
|
331
|
+
item["link"] = link
|
|
332
|
+
if disabled and new:
|
|
333
|
+
item["disabled"] = True # Uniflow disabled it; the update keeps it disabled.
|
|
334
|
+
items.append(item)
|
|
249
335
|
output.append({"name": name, "target": key, "agents": target_info["agents"], "action": action})
|
|
250
336
|
if command == "uninstall":
|
|
251
337
|
after[key]["installed"].pop(name)
|
|
@@ -267,7 +353,7 @@ class Store:
|
|
|
267
353
|
if self.state(target) != state:
|
|
268
354
|
raise CrowError("changed_during_plan", "O estado mudou após o preview; execute novamente.")
|
|
269
355
|
for item in plan["items"]:
|
|
270
|
-
if
|
|
356
|
+
if not self.matches(Path(item["target"]) / item["name"], item):
|
|
271
357
|
raise CrowError("changed_during_plan", "Um destino mudou após o preview; execute novamente.")
|
|
272
358
|
txid = uuid.uuid4().hex
|
|
273
359
|
data = {k: plan[k] for k in ("before", "after", "items")}
|
|
@@ -285,6 +371,8 @@ class Store:
|
|
|
285
371
|
if item["new"] is not None:
|
|
286
372
|
new = self.work(item["target"], txid) / "new" / item["name"]
|
|
287
373
|
plan["packages"][item["name"]].materialize(new)
|
|
374
|
+
if item.get("disabled"):
|
|
375
|
+
os.replace(new / "SKILL.md", new / DISABLED)
|
|
288
376
|
if snapshot(new) != item["new"]:
|
|
289
377
|
raise CrowError("staging_failed", "Cópia staged não corresponde ao pacote validado.")
|
|
290
378
|
atomic_json(journal, data)
|
|
@@ -293,9 +381,9 @@ class Store:
|
|
|
293
381
|
ensure_dir(target)
|
|
294
382
|
dest = target / item["name"]
|
|
295
383
|
work = self.work(target, txid)
|
|
296
|
-
if
|
|
384
|
+
if not self.matches(dest, item):
|
|
297
385
|
raise CrowError("changed_during_install", "Destino alterado durante instalação.")
|
|
298
|
-
if item["old"] is not None:
|
|
386
|
+
if item["old"] is not None or item.get("link") is not None: # a link moves as itself, never its target
|
|
299
387
|
os.replace(dest, work / "old" / item["name"])
|
|
300
388
|
if item["new"] is not None:
|
|
301
389
|
os.replace(work / "new" / item["name"], dest)
|
|
@@ -315,6 +403,12 @@ class Store:
|
|
|
315
403
|
raise
|
|
316
404
|
return txid
|
|
317
405
|
|
|
406
|
+
@staticmethod
|
|
407
|
+
def matches(dest, item):
|
|
408
|
+
if item.get("link") is not None:
|
|
409
|
+
return same_link(dest, item["link"])
|
|
410
|
+
return snapshot(dest) == item["old"]
|
|
411
|
+
|
|
318
412
|
def _restore(self, data):
|
|
319
413
|
"""Validate every target/backup before changing any during recovery."""
|
|
320
414
|
operations = []
|
|
@@ -323,8 +417,18 @@ class Store:
|
|
|
323
417
|
if current != before and current != data["after"][target]:
|
|
324
418
|
raise CrowError("recovery_conflict", "Manifesto mudou após a transação; recuperação recusada.")
|
|
325
419
|
for item in reversed(data["items"]):
|
|
326
|
-
dest = checked(Path(item["target"]) / item["name"]
|
|
327
|
-
old = self.
|
|
420
|
+
dest = checked(Path(item["target"])) / item["name"]
|
|
421
|
+
old = self.existing_work(item["target"], data["id"]) / "old" / item["name"]
|
|
422
|
+
if item.get("link") is not None:
|
|
423
|
+
if same_link(old, item["link"]):
|
|
424
|
+
current = None if not os.path.lexists(dest) else ("link" if dest.is_symlink() else snapshot(dest))
|
|
425
|
+
if current not in (None, item["new"]):
|
|
426
|
+
raise CrowError("recovery_conflict", "Destino do link substituído foi alterado; recuperação recusada.")
|
|
427
|
+
operations.append((dest, old))
|
|
428
|
+
elif not same_link(dest, item["link"]):
|
|
429
|
+
raise CrowError("recovery_conflict", "Link original ausente; recuperação recusada.")
|
|
430
|
+
continue
|
|
431
|
+
dest = checked(dest)
|
|
328
432
|
backup = snapshot(old)
|
|
329
433
|
current = snapshot(dest)
|
|
330
434
|
if backup is not None:
|
|
@@ -342,7 +446,9 @@ class Store:
|
|
|
342
446
|
data["phase"] = "restoring"
|
|
343
447
|
atomic_json(self.root / "transactions" / (data["id"] + ".json"), data)
|
|
344
448
|
for dest, backup in operations:
|
|
345
|
-
if dest.
|
|
449
|
+
if dest.is_symlink():
|
|
450
|
+
dest.unlink()
|
|
451
|
+
elif dest.exists():
|
|
346
452
|
shutil.rmtree(dest)
|
|
347
453
|
if backup is not None:
|
|
348
454
|
os.replace(backup, dest)
|
|
@@ -390,10 +496,30 @@ class Store:
|
|
|
390
496
|
"note": "Nome externo ao catálogo: não gerenciado nem removido por este CLI."})
|
|
391
497
|
continue
|
|
392
498
|
record = state["installed"].get(name)
|
|
499
|
+
link = target / name
|
|
500
|
+
store = uniflow_store(self.home)
|
|
501
|
+
if link.is_symlink() and target == store:
|
|
502
|
+
resolved = os.path.realpath(link)
|
|
503
|
+
managed = self.managed_target(resolved, name)
|
|
504
|
+
rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
|
|
505
|
+
"status": "external_link", "revision": None, "activation_verified": False,
|
|
506
|
+
"note": "Link no store do Uniflow para " + resolved +
|
|
507
|
+
(" (cópia gerenciada pelo crow-skills)" if managed else " (não gerenciado)") +
|
|
508
|
+
". Troque só o link por uma cópia gerenciada: crow-skills install " + name +
|
|
509
|
+
" --agents uniflow --global" + ("" if managed else " --replace") + " --yes"})
|
|
510
|
+
continue
|
|
511
|
+
if (record is None and link.is_symlink() and not (store / name).is_symlink() and (store / name).is_dir()
|
|
512
|
+
and os.path.realpath(target) != os.path.realpath(store)
|
|
513
|
+
and os.path.realpath(link) == os.path.realpath(store / name)):
|
|
514
|
+
rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
|
|
515
|
+
"status": "uniflow_link", "revision": None, "activation_verified": False,
|
|
516
|
+
"note": "Link projetado pelo Uniflow a partir do seu store; gerenciado em --agents uniflow."})
|
|
517
|
+
continue
|
|
393
518
|
try:
|
|
394
519
|
entries = snapshot(target / name)
|
|
395
520
|
status = ("absent" if entries is None else "unmanaged") if record is None else (
|
|
396
|
-
"missing" if entries is None else "installed" if entries == record["entries"] else
|
|
521
|
+
"missing" if entries is None else "installed" if entries == record["entries"] else
|
|
522
|
+
"disabled" if entries == disabled_variant(record["entries"]) else "modified")
|
|
397
523
|
except CrowError:
|
|
398
524
|
status = "unsafe"
|
|
399
525
|
rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
|
package/docs/cli.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# crow-skills CLI 0.
|
|
1
|
+
# crow-skills CLI 0.7.0
|
|
2
2
|
|
|
3
3
|
CLI local-first para humanos e agentes. Python 3.10+, sem bibliotecas Python externas; Git é necessário para fontes em clone, não para o snapshot npm. O launcher npm requer Node 18+. Mutações suportadas em macOS/Linux (locks POSIX). Não instala o agente, não executa scripts das skills e não muda modelos, prompts base ou permissões. Instruções de uso das skills (o guia do agente, abaixo) são gravadas pelo comando `guide` e, por padrão, por `install`/`update`/`uninstall` em `--global` quando a crow-behaviour fica instalada para um agente com mecanismo verificado; `--no-guide` desliga isso. Os mesmos comandos também aplicam as [integrações de interface](#integrações-de-interface-integrations) (status line do Claude Code e do Codex, segmento tmux); `--no-integrations` desliga. `crow-skills progress` mostra o [painel de progresso](#progresso-progress) da execução crow-behaviour ativa.
|
|
4
4
|
|
|
@@ -31,7 +31,7 @@ Para testar/distribuir o arquivo local sem registry:
|
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
33
|
npm pack
|
|
34
|
-
npm install -g ./andespindola-crow-skills-0.
|
|
34
|
+
npm install -g ./andespindola-crow-skills-0.7.0.tgz --ignore-scripts
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
O pacote contém o CLI Python e todas as skills do commit, com `release.json` de hashes. Não requer acesso ao GitHub ou clone para list/install. O launcher Node encaminha argumentos sem shell para Python em modo isolado, evitando imports pelo diretório do chamador/PYTHONPATH. `CROW_SKILLS_PYTHON` pode apontar para outro executável Python 3.10+. Não há dependências npm nem hooks de instalação que instalem skills automaticamente.
|
|
@@ -97,7 +97,7 @@ crow-skills recover --yes
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
- Manifestos/locks/journals ficam sob `~/.local/state/crow-skills/`, separados da descoberta de skills. Não se escreve `manifest.json` dentro de `.claude/skills`.
|
|
100
|
-
- 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. Backups são privados e retidos; não há expiração automática nesta versão.
|
|
100
|
+
- 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.
|
|
101
101
|
- 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`.
|
|
102
102
|
- `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.
|
|
103
103
|
- `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.
|
|
@@ -135,11 +135,35 @@ Erros retornam `ok:false` e `error.code/message`. Saídas reais incluem alteraç
|
|
|
135
135
|
| gemini | `~/.gemini/skills` | `.gemini/skills` |
|
|
136
136
|
| devin | `~/.config/devin/skills` (ou XDG_CONFIG_HOME) | `.devin/skills` |
|
|
137
137
|
| shared | `~/.agents/skills` | `.agents/skills` |
|
|
138
|
+
| uniflow | store do Uniflow: `~/.config/uniflow/skills` (veja [Uniflow](#uniflow)) | não existe; recusado com `unsupported_scope` |
|
|
138
139
|
|
|
139
140
|
Codex/shared são deduplicados quando apontam para o mesmo diretório. O diretório compartilhado também pode ser lido por outros hosts; não é isolamento entre agentes. `.codex/skills` é uma localização legada encontrada em ambientes existentes; o adaptador usa a localização moderna documentada. Para um destino legado específico, use `--dest` explicitamente.
|
|
140
141
|
|
|
141
142
|
`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 hook ou system prompt base é alterado para forçar uso; o guia abaixo, aplicado por padrão em `--global` e desligável com `--no-guide`, é aditivo.
|
|
142
143
|
|
|
144
|
+
### Uniflow
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
crow-skills install --all --agents uniflow --global --dry-run
|
|
148
|
+
crow-skills install --all --agents uniflow --global --yes
|
|
149
|
+
crow-skills status --agents uniflow
|
|
150
|
+
crow-skills update --agents uniflow --global --yes
|
|
151
|
+
crow-skills uninstall --all --agents uniflow --global --yes
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
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.
|
|
155
|
+
|
|
156
|
+
- 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.
|
|
157
|
+
- 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.
|
|
158
|
+
- 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`.
|
|
159
|
+
- 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`).
|
|
160
|
+
- 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.
|
|
161
|
+
- Só `--global`: o Uniflow não tem store de projeto (`--project` falha com `unsupported_scope`).
|
|
162
|
+
- 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.
|
|
163
|
+
- Detecção: `agents` marca `detected` quando `uniflow` está no PATH (`executable_found`) ou o diretório de configuração existe (`config_found`).
|
|
164
|
+
- 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`). No codex é pior: o crow-skills instala em `~/.agents/skills`, o Uniflow liga o store em `~/.codex/skills` e o Codex lê os dois, vendo duas skills de mesmo nome (`effect: both`). `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.
|
|
165
|
+
- 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`.
|
|
166
|
+
|
|
143
167
|
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).
|
|
144
168
|
|
|
145
169
|
## Instruções para o agente (guide)
|
|
@@ -159,7 +183,7 @@ Aplicação automática (padrão em `install`, `update` e `uninstall` com `--glo
|
|
|
159
183
|
|
|
160
184
|
- 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 compartilhado `~/.agents/skills`, que é o 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.
|
|
161
185
|
- 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.
|
|
162
|
-
- `shared`, `--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.
|
|
186
|
+
- `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.
|
|
163
187
|
- `--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`.
|
|
164
188
|
|
|
165
189
|
| Agente | Mecanismo (usuário) | Propriedade |
|
|
@@ -171,8 +195,8 @@ Aplicação automática (padrão em `install`, `update` e `uninstall` com `--glo
|
|
|
171
195
|
|
|
172
196
|
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`.
|
|
173
197
|
|
|
174
|
-
- Só `--global` é suportado. No comando `guide`, `--project`, `--dest` e `
|
|
175
|
-
- 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/` ou nos alvos desses links. Não usa mecanismos que substituem o prompt base (como `model_instructions_file` ou `AGENTS.override.md`).
|
|
198
|
+
- Só `--global` é suportado. No comando `guide`, `--project`, `--dest`, `shared` e `uniflow` são recusados com `unsupported_guide`, porque não há mecanismo verificado.
|
|
199
|
+
- 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`).
|
|
176
200
|
- 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.
|
|
177
201
|
- 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.
|
|
178
202
|
- 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.
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"README.md": {
|
|
4
4
|
"executable": false,
|
|
5
5
|
"kind": "file",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "c5093abfec8d8ee55d73e81fa2130f02c93a1fb53c998fc3dd9bafc4d989bd16"
|
|
7
7
|
},
|
|
8
8
|
"bin/npm-crow-skills.cjs": {
|
|
9
9
|
"executable": true,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"crow_skills/__init__.py": {
|
|
19
19
|
"executable": false,
|
|
20
20
|
"kind": "file",
|
|
21
|
-
"sha256": "
|
|
21
|
+
"sha256": "b7e23c635664d60f1afc60f98f3e68d7558fe4357973bb5b0d4592e7822a69aa"
|
|
22
22
|
},
|
|
23
23
|
"crow_skills/__main__.py": {
|
|
24
24
|
"executable": false,
|
|
@@ -28,17 +28,17 @@
|
|
|
28
28
|
"crow_skills/agents.py": {
|
|
29
29
|
"executable": false,
|
|
30
30
|
"kind": "file",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "b14c37d2f1becfe38c74411fc413608b375c05bd6961a0757e3ec085ec2e1f66"
|
|
32
32
|
},
|
|
33
33
|
"crow_skills/cli.py": {
|
|
34
34
|
"executable": false,
|
|
35
35
|
"kind": "file",
|
|
36
|
-
"sha256": "
|
|
36
|
+
"sha256": "fd3c4e63be95f0a616932438f1d2648facffabf825a89bb15d9f1a466d33b075"
|
|
37
37
|
},
|
|
38
38
|
"crow_skills/guide.py": {
|
|
39
39
|
"executable": false,
|
|
40
40
|
"kind": "file",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "cabfe0809c0fc607b7deac0cd8df9025acaadddf4deb05f45288158dc51b0391"
|
|
42
42
|
},
|
|
43
43
|
"crow_skills/integrations.py": {
|
|
44
44
|
"executable": false,
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"crow_skills/storage.py": {
|
|
64
64
|
"executable": false,
|
|
65
65
|
"kind": "file",
|
|
66
|
-
"sha256": "
|
|
66
|
+
"sha256": "bf3c4bcb83888b74c72773aa1d0c0def4b3ba08f0c7bdbb918d2c549438e4d83"
|
|
67
67
|
},
|
|
68
68
|
"docs/cli-security.md": {
|
|
69
69
|
"executable": false,
|
|
@@ -73,12 +73,12 @@
|
|
|
73
73
|
"docs/cli.md": {
|
|
74
74
|
"executable": false,
|
|
75
75
|
"kind": "file",
|
|
76
|
-
"sha256": "
|
|
76
|
+
"sha256": "dd869d0e4e3354954436e78d0bde0254ee86da752195a7f55d9e7a8e5a628bdb"
|
|
77
77
|
},
|
|
78
78
|
"package.json": {
|
|
79
79
|
"executable": false,
|
|
80
80
|
"kind": "file",
|
|
81
|
-
"sha256": "
|
|
81
|
+
"sha256": "ae9338cf496e3fb2691abed08474ad6a85c85932d5bf66c9a5c3a6b106208d2e"
|
|
82
82
|
},
|
|
83
83
|
"skills/crow-accessibility/SKILL.md": {
|
|
84
84
|
"executable": false,
|
|
@@ -631,7 +631,7 @@
|
|
|
631
631
|
"sha256": "4b921ea30ae9b731b5210581873e3d180e830cadfdc02bdbb5b3419ec39baaab"
|
|
632
632
|
}
|
|
633
633
|
},
|
|
634
|
-
"revision": "
|
|
634
|
+
"revision": "07e8fbd997cfbeb645fb0b986725934ebb243ce8",
|
|
635
635
|
"schema_version": 1,
|
|
636
|
-
"version": "0.
|
|
636
|
+
"version": "0.7.0"
|
|
637
637
|
}
|