@andespindola/crow-skills 0.27.1 → 0.29.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 +4 -4
- package/catalog.json +26 -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/cli.py +12 -157
- package/crow_skills/guide.py +33 -65
- 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 +30 -20
- package/skills/crow-behaviour/SKILL.md +4 -2
- package/skills/crow-behaviour/references/broadcast.md +1 -1
- package/skills/crow-behaviour/references/micro-agents.md +12 -5
- package/skills/crow-behaviour/references/orchestration.md +1 -1
- package/skills/crow-ml/SKILL.md +35 -0
- package/skills/crow-model-level/SKILL.md +35 -0
package/crow_skills/cli.py
CHANGED
|
@@ -20,7 +20,7 @@ from . import machine
|
|
|
20
20
|
from . import mcp
|
|
21
21
|
from . import ui
|
|
22
22
|
from .agents import (AGENTS, destinations, inventory, legacy_shared_dir,
|
|
23
|
-
refuse_implausible_destination
|
|
23
|
+
refuse_implausible_destination)
|
|
24
24
|
from .source import CrowError, Source, valid_name
|
|
25
25
|
from .storage import Store, checked
|
|
26
26
|
|
|
@@ -32,7 +32,7 @@ TRUST_HELP = "Não habilitar/aprovar os hooks do crow-skills nos CLIs (padrão:
|
|
|
32
32
|
|
|
33
33
|
# Per-command help: what it is for and real examples (crow-skills help <cmd> or <cmd> --help).
|
|
34
34
|
COMMANDS = {
|
|
35
|
-
"setup": ("Detecta os CLIs de agente desta máquina (Claude Code, Codex, Gemini CLI, Antigravity, Devin,
|
|
35
|
+
"setup": ("Detecta os CLIs de agente desta máquina (Claude Code, Codex, Gemini CLI, Antigravity, Devin, tmux) e "
|
|
36
36
|
"configura cada um no escopo do usuário: todas as skills do catálogo, guia, integrações, hooks do modo crow "
|
|
37
37
|
"habilitados, modo crow e autoupdate. Rodar de novo só corrige o que mudou.",
|
|
38
38
|
["crow-skills setup --dry-run", "crow-skills setup --yes", "crow-skills setup --agents claude,codex --yes",
|
|
@@ -289,7 +289,6 @@ def scope(args, home, interactive=False, read=False):
|
|
|
289
289
|
agents = prompt("Agentes separados por vírgula (" + ",".join(AGENTS) + "):")
|
|
290
290
|
targets = destinations(agents, home, project, custom)
|
|
291
291
|
if not read:
|
|
292
|
-
refuse_store_as_cli_dir(targets, home)
|
|
293
292
|
refuse_implausible_destination(targets, home)
|
|
294
293
|
return targets
|
|
295
294
|
|
|
@@ -375,7 +374,6 @@ def guide_sync(args, home, store, command, targets, plan, source):
|
|
|
375
374
|
if agent not in selected and not (record and path in touched):
|
|
376
375
|
continue
|
|
377
376
|
added, removed = touched.get(path, (set(), set()))
|
|
378
|
-
removed = removed - projection.projected(agent, home, store) # still provided by Uniflow's projection
|
|
379
377
|
after = sorted((set(guide.managed_skills(agent, home, store)) | added) - removed)
|
|
380
378
|
if mech["kind"] == "toml" and guide.tomllib is None:
|
|
381
379
|
report["skipped"].append({"agent": agent, "reason": "Python 3.11+ (tomllib) é necessário para editar config.toml; guia não alterado."})
|
|
@@ -389,7 +387,7 @@ def guide_sync(args, home, store, command, targets, plan, source):
|
|
|
389
387
|
report["skipped"].append({"agent": agent, "reason": "crow-behaviour não ficará instalada para " + agent + "; guia não aplicado."})
|
|
390
388
|
# Um guia que não pode ser gravado não derruba a instalação das skills. Desde que o guia também
|
|
391
389
|
# resolve o caminho físico, o caso que sobra aqui é a área PROTEGIDA (prompt compartilhado,
|
|
392
|
-
#
|
|
390
|
+
# alvos dos prompts), que `guide.protect()` recusa com protected_path: aquele agente entra em
|
|
393
391
|
# "skipped", com o motivo, e o resto segue.
|
|
394
392
|
for agent in list(actions):
|
|
395
393
|
mechanism = guide.MECHANISMS.get(agent)
|
|
@@ -447,7 +445,6 @@ def behaviour_agents(home, store, plan=None):
|
|
|
447
445
|
path = os.path.abspath(destinations(agent, home)[0]["path"])
|
|
448
446
|
added, removed = touched.get(path, (set(), set()))
|
|
449
447
|
try:
|
|
450
|
-
removed = removed - projection.projected(agent, home, store)
|
|
451
448
|
managed = set(guide.managed_skills(agent, home, store))
|
|
452
449
|
except CrowError:
|
|
453
450
|
# An unreadable skills root (e.g. a dotfiles symlink) holds nothing this CLI manages;
|
|
@@ -852,56 +849,6 @@ def linked_guides(store, txid):
|
|
|
852
849
|
return [t["id"] for t in store.transactions() if t.get("kind") == "guide" and t.get("linked") == txid and t["phase"] == "committed"]
|
|
853
850
|
|
|
854
851
|
|
|
855
|
-
# What a native copy means once Uniflow links its store into each CLI's user skills dir.
|
|
856
|
-
# Every CLI, codex included since 0.15.0, has its skills installed in its OWN dir, the same one
|
|
857
|
-
# Uniflow links into, so a native copy of a name always just wins there (no second directory).
|
|
858
|
-
NATIVE_EFFECT = {
|
|
859
|
-
"claude": "skipped", "gemini": "skipped", "devin": "skipped",
|
|
860
|
-
"codex": "skipped",
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
def uniflow_notice(home, store, targets, plan):
|
|
865
|
-
"""Installing into the Uniflow store while native crow-* copies remain. Uniflow does
|
|
866
|
-
not link a name a CLI already has in the same dir, so that copy (possibly another
|
|
867
|
-
revision) wins there. Report it and the explicit command; never remove anything."""
|
|
868
|
-
if not any("uniflow" in t["agents"] for t in targets):
|
|
869
|
-
return None
|
|
870
|
-
names = sorted({c["name"] for c in plan["changes"] if "uniflow" in c["agents"]})
|
|
871
|
-
native, uninspected = {}, []
|
|
872
|
-
for agent in ("claude", "codex", "gemini", "devin"):
|
|
873
|
-
target = destinations(agent, home)[0]
|
|
874
|
-
try:
|
|
875
|
-
rows = store.status([target], names)
|
|
876
|
-
except (CrowError, OSError) as exc:
|
|
877
|
-
# Informative only: an unreadable native dir (e.g. a dotfiles symlink) never blocks the install.
|
|
878
|
-
uninspected.append({"agent": agent, "target": target["path"], "reason": str(exc) or "falha de leitura"})
|
|
879
|
-
continue
|
|
880
|
-
for row in rows:
|
|
881
|
-
if row["name"] in names and row["status"] in ("installed", "modified", "unmanaged"):
|
|
882
|
-
native.setdefault(row["target"], {"target": row["target"], "agents": target["agents"],
|
|
883
|
-
"effect": NATIVE_EFFECT[agent], "skills": []})
|
|
884
|
-
native[row["target"]]["skills"].append({"name": row["name"], "status": row["status"]})
|
|
885
|
-
if not native:
|
|
886
|
-
return {"native_duplicates": [], "uninspected": uninspected}
|
|
887
|
-
# One command per destination with only the names physically managed there: crossing names and agents
|
|
888
|
-
# would reach a Uniflow link (or an absent name) in another CLI and abort the uninstall.
|
|
889
|
-
# Destinations with the same names share one command (the usual case keeps its single command).
|
|
890
|
-
groups = {}
|
|
891
|
-
for item in native.values():
|
|
892
|
-
managed = tuple(sorted({s["name"] for s in item["skills"] if s["status"] != "unmanaged"}))
|
|
893
|
-
if managed:
|
|
894
|
-
groups.setdefault(managed, []).extend(a for a in item["agents"] if a not in groups.get(managed, []))
|
|
895
|
-
commands = ["crow-skills uninstall " + " ".join(names_) + " --agents " + ",".join(sorted(agents_)) + " --global"
|
|
896
|
-
for names_, agents_ in groups.items()]
|
|
897
|
-
return {"native_duplicates": list(native.values()), "uninspected": uninspected,
|
|
898
|
-
"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 "
|
|
899
|
-
"store. Desde a 0.15.0 nenhum destino produz effect=both: todo CLI instala no diretório dele, o mesmo em que o "
|
|
900
|
-
"Uniflow projeta o store, então não há uma segunda raiz em que o mesmo nome apareça de novo. Nada foi removido. "
|
|
901
|
-
"Remover a cópia nativa de crow-behaviour remove também o guia daquele agente.",
|
|
902
|
-
"suggested_commands": commands, "suggested_command": " && ".join(commands) if commands else None}
|
|
903
|
-
|
|
904
|
-
|
|
905
852
|
def pick(source):
|
|
906
853
|
"""Terminal picker without dependencies: a group number or name, todas, or skill names. Default: essencial."""
|
|
907
854
|
available = groups_mod.load(source)
|
|
@@ -931,19 +878,6 @@ def pick(source):
|
|
|
931
878
|
return names, ",".join(chosen_groups) or None
|
|
932
879
|
|
|
933
880
|
|
|
934
|
-
def codex_projection_split(home, store, targets, names):
|
|
935
|
-
"""The user-scope codex target and which of the names Codex already sees via Uniflow (install skips them)."""
|
|
936
|
-
target = next((t for t in targets if projection.native_target("codex", t, home)), None)
|
|
937
|
-
if target is None or set(target["agents"]) - {"codex"}:
|
|
938
|
-
return None # a dir shared with another agent is not Codex's own projected dir
|
|
939
|
-
projected = projection.projected("codex", home, store)
|
|
940
|
-
if not projected:
|
|
941
|
-
return None
|
|
942
|
-
dupes = projection.duplicates(home, store)
|
|
943
|
-
return {"target": target, "skip": set(names) & projected,
|
|
944
|
-
"duplicates": sorted(set(dupes["remove"]) | set(dupes["modified"]))}
|
|
945
|
-
|
|
946
|
-
|
|
947
881
|
def merge_plans(plans):
|
|
948
882
|
merged = {"before": {}, "after": {}, "items": [], "packages": {}, "changes": []}
|
|
949
883
|
for plan in plans:
|
|
@@ -955,29 +889,6 @@ def merge_plans(plans):
|
|
|
955
889
|
return merged
|
|
956
890
|
|
|
957
891
|
|
|
958
|
-
def unmanaged_projections(store, home, rows):
|
|
959
|
-
"""A Uniflow link to a store entry crow-skills does not manage is not an install (same rule as setup)."""
|
|
960
|
-
if not any(r["status"] == "uniflow_link" for r in rows):
|
|
961
|
-
return rows
|
|
962
|
-
managed = guide.store_managed(home, store)
|
|
963
|
-
disabled = machine.store_disabled(home, store)
|
|
964
|
-
broken = guide.store_broken(home, store)
|
|
965
|
-
for row in rows:
|
|
966
|
-
if row["status"] == "uniflow_link" and row["name"] in broken:
|
|
967
|
-
# The store entry this link resolves to was edited or lost files: not a working install.
|
|
968
|
-
row.update(status="modified", projection="uniflow_link_modified",
|
|
969
|
-
note=guide.BROKEN_REASON[0].upper() + guide.BROKEN_REASON[1:] + ". Para corrigir: " + guide.broken_fix(row["name"]))
|
|
970
|
-
elif row["status"] == "uniflow_link" and row["name"] in disabled:
|
|
971
|
-
# Managed by crow-skills but disabled by Uniflow: reported as such, never as unmanaged (no --replace).
|
|
972
|
-
row.update(status="disabled", projection="uniflow_link_disabled",
|
|
973
|
-
note="Desabilitada no Uniflow (SKILL.md.disabled no store); reabilite no Uniflow para usar.")
|
|
974
|
-
elif row["status"] == "uniflow_link" and row["name"] not in managed:
|
|
975
|
-
row.update(status="unmanaged", projection="uniflow_link_unmanaged",
|
|
976
|
-
note="Link do Uniflow para um pacote do store que o crow-skills não gerencia; não conta como instalada. "
|
|
977
|
-
"Revise o store e, para gerenciá-lo com backup: crow-skills setup --agents uniflow --replace --yes")
|
|
978
|
-
return rows
|
|
979
|
-
|
|
980
|
-
|
|
981
892
|
def group_coverage(rows):
|
|
982
893
|
"""Per destination, which catalog groups are fully or partially installed (best effort)."""
|
|
983
894
|
try:
|
|
@@ -987,7 +898,7 @@ def group_coverage(rows):
|
|
|
987
898
|
by_target = {}
|
|
988
899
|
for row in rows:
|
|
989
900
|
item = by_target.setdefault(row["target"], {"target": row["target"], "agents": row["agents"], "names": set()})
|
|
990
|
-
if row["name"] and row["status"] in ("installed", "modified"
|
|
901
|
+
if row["name"] and row["status"] in ("installed", "modified"):
|
|
991
902
|
item["names"].add(row["name"])
|
|
992
903
|
return [{"target": v["target"], "agents": v["agents"],
|
|
993
904
|
"groups": [c for c in groups_mod.coverage(available, v["names"]) if c["state"] != "absent"]}
|
|
@@ -1002,8 +913,6 @@ SKILL_STATE_WORDS = {
|
|
|
1002
913
|
"modificada": "editada no disco",
|
|
1003
914
|
"desativada": "instalada, mas desabilitada",
|
|
1004
915
|
"ausente": "registrada e não encontrada no disco",
|
|
1005
|
-
"via_uniflow": "projetada do seu store do Uniflow (gerenciada em --agents uniflow)",
|
|
1006
|
-
"link_externo": "link no store do Uniflow para fora dele",
|
|
1007
916
|
"nao_gerenciada": "não gerenciada por este CLI",
|
|
1008
917
|
"indeterminada": "não foi possível ler o destino",
|
|
1009
918
|
}
|
|
@@ -1024,15 +933,6 @@ def skill_state(row, source):
|
|
|
1024
933
|
return "desativada"
|
|
1025
934
|
if status == "missing":
|
|
1026
935
|
return "ausente"
|
|
1027
|
-
if status == "uniflow_link":
|
|
1028
|
-
# Projeção SAUDÁVEL do store do Uniflow: é a topologia normal de quem instala em uniflow e
|
|
1029
|
-
# deixa os CLIs lerem de lá, e o resto do CLI já a conta como instalada (group_coverage).
|
|
1030
|
-
# Chamar isso de "não gerenciada" assustava sem motivo — 33 skills em 4 agentes, aqui.
|
|
1031
|
-
# Projeção quebrada, desabilitada ou para pacote não gerenciado não chega como uniflow_link:
|
|
1032
|
-
# unmanaged_projections() a reescreve antes (modified/disabled/unmanaged).
|
|
1033
|
-
return "via_uniflow"
|
|
1034
|
-
if status == "external_link":
|
|
1035
|
-
return "link_externo"
|
|
1036
936
|
if status in ("unmanaged", "absent"):
|
|
1037
937
|
return "nao_gerenciada"
|
|
1038
938
|
return "indeterminada" # "unsafe" e qualquer estado novo: nunca contado como em dia
|
|
@@ -1089,9 +989,7 @@ def skills_command(args, home, store):
|
|
|
1089
989
|
"""Somente leitura: versão e estado de cada skill instalada, contra a fonte atual."""
|
|
1090
990
|
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"))
|
|
1091
991
|
targets = scope(args, home, read=True)
|
|
1092
|
-
|
|
1093
|
-
# pacote não gerenciado não pode passar por projeção saudável.
|
|
1094
|
-
rows = unmanaged_projections(store, home, store.status(targets))
|
|
992
|
+
rows = store.status(targets)
|
|
1095
993
|
items = {}
|
|
1096
994
|
for row in rows:
|
|
1097
995
|
if not row.get("name"):
|
|
@@ -1231,7 +1129,7 @@ def mcp_setup_extras(args, home, result):
|
|
|
1231
1129
|
supported = {host for host, _rel, _kind in mcp.HOSTS}
|
|
1232
1130
|
for entry in result.get("entries", []):
|
|
1233
1131
|
agent = entry["agent"]
|
|
1234
|
-
selected = set(entry.get("changed", []) + entry.get("unchanged", [])
|
|
1132
|
+
selected = set(entry.get("changed", []) + entry.get("unchanged", []))
|
|
1235
1133
|
if entry.get("error") or "crow-uny-mcp" not in selected:
|
|
1236
1134
|
continue
|
|
1237
1135
|
if agent not in supported:
|
|
@@ -1337,7 +1235,7 @@ def run(args):
|
|
|
1337
1235
|
return skills_command(args, home, store)
|
|
1338
1236
|
if args.command in ("status", "doctor"):
|
|
1339
1237
|
targets = scope(args, home, read=True)
|
|
1340
|
-
rows =
|
|
1238
|
+
rows = store.status(targets)
|
|
1341
1239
|
pending = [t["id"] for t in store.pending()]
|
|
1342
1240
|
global_view = not (getattr(args, "project", None) or getattr(args, "dest", None))
|
|
1343
1241
|
names = [n.strip() for n in getattr(args, "agents", "").split(",")] if getattr(args, "agents", None) else list(guide.MECHANISMS)
|
|
@@ -1351,23 +1249,9 @@ def run(args):
|
|
|
1351
1249
|
"mode": hook.read_mode(home), "activation_verified": False,
|
|
1352
1250
|
"reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
|
|
1353
1251
|
if any(projection.native_target("codex", t, home) for t in targets):
|
|
1354
|
-
|
|
1355
|
-
dupes = projection.duplicates(home, store)
|
|
1356
|
-
names_ = dupes["remove"] + dupes["modified"] + dupes["shared"]
|
|
1357
|
-
if names_:
|
|
1358
|
-
# A warning, not unhealthy: both copies are intact and work; Codex just lists them twice.
|
|
1359
|
-
result.setdefault("warnings", []).append({"code": "codex_duplicates", "skills": names_,
|
|
1360
|
-
"message": "O Codex vê " + str(len(names_)) + " skill(s) duas vezes: cópia antiga na pasta "
|
|
1361
|
-
"compartilhada " + str(legacy_shared_dir(home)) + " e a instalação em "
|
|
1362
|
-
+ str(projection.codex_skills_dir(home)) + ". O crow-skills não usa mais a pasta "
|
|
1363
|
-
"compartilhada; remover as cópias de lá encerra a duplicação.",
|
|
1364
|
-
"fix": "crow-skills uninstall " + ",".join(names_) + " --dest "
|
|
1365
|
-
+ str(legacy_shared_dir(home)) + " --yes"
|
|
1366
|
-
+ (" --overwrite-modified" if dupes["modified"] else "")})
|
|
1367
|
-
leftovers = [n for n in projection.legacy_leftovers(home, store) if n not in names_]
|
|
1252
|
+
leftovers = projection.legacy_leftovers(home, store)
|
|
1368
1253
|
if leftovers:
|
|
1369
|
-
|
|
1370
|
-
# e uma instalação feita pela 0.14.0 ficaria invisível aqui, ainda carregada pelo Codex.
|
|
1254
|
+
|
|
1371
1255
|
result.setdefault("warnings", []).append(
|
|
1372
1256
|
{"code": "codex_legacy_shared", "skills": leftovers,
|
|
1373
1257
|
"message": "O crow-skills ainda tem " + str(len(leftovers)) + " skill(s) instalada(s) na pasta "
|
|
@@ -1490,17 +1374,7 @@ def run(args):
|
|
|
1490
1374
|
for name in list(names):
|
|
1491
1375
|
source.require(name)
|
|
1492
1376
|
names.extend(n for n in source.catalog[name]["next"] if n not in names)
|
|
1493
|
-
|
|
1494
|
-
if args.command == "install" and not (getattr(args, "project", None) or getattr(args, "dest", None)):
|
|
1495
|
-
codex_view = codex_projection_split(home, store, targets, names)
|
|
1496
|
-
if codex_view and codex_view["skip"]:
|
|
1497
|
-
# Codex already sees these through Uniflow's links in ~/.codex/skills: no native copy for codex.
|
|
1498
|
-
plans = [store.plan(args.command, [n for n in names if n not in codex_view["skip"]] if t is codex_view["target"] else names,
|
|
1499
|
-
[t], source, getattr(args, "replace", False), args.overwrite_modified)
|
|
1500
|
-
for t in targets if t is not codex_view["target"] or set(names) - codex_view["skip"]]
|
|
1501
|
-
plan = merge_plans(plans)
|
|
1502
|
-
else:
|
|
1503
|
-
plan = store.plan(args.command, names, targets, source, getattr(args, "replace", False), args.overwrite_modified)
|
|
1377
|
+
plan = store.plan(args.command, names, targets, source, getattr(args, "replace", False), args.overwrite_modified)
|
|
1504
1378
|
guides, gplan = guide_sync(args, home, store, args.command, targets, plan, source)
|
|
1505
1379
|
ui = integrations_preview(args, home, store, plan)
|
|
1506
1380
|
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
@@ -1513,20 +1387,6 @@ def run(args):
|
|
|
1513
1387
|
# Read-only dependency/conflict preview precedes every mutation, including custom targets.
|
|
1514
1388
|
mcp_guard_runtime_removal(home, plan["items"], [r["agent"] for r in summary["mcp"]["changes"] if r["action"] == "restore"])
|
|
1515
1389
|
mcp_before_uninstall(home, summary["mcp"], dry_run=True)
|
|
1516
|
-
notice = uniflow_notice(home, store, targets, plan) if args.command != "uninstall" else None
|
|
1517
|
-
if notice is not None:
|
|
1518
|
-
summary["uniflow"] = notice
|
|
1519
|
-
if codex_view and (codex_view["skip"] or codex_view["duplicates"]):
|
|
1520
|
-
summary["codex_projection"] = {
|
|
1521
|
-
"via_uniflow": sorted(codex_view["skip"]), "duplicates": codex_view["duplicates"],
|
|
1522
|
-
"note": "O Codex já vê estas skills pelo link do Uniflow em " + str(projection.codex_skills_dir(home)) +
|
|
1523
|
-
"; nenhuma cópia é criada para o codex. As cópias que ficaram na pasta compartilhada antiga "
|
|
1524
|
-
+ str(legacy_shared_dir(home)) + " duplicam a projeção e não são removidas por install; remova-as com o "
|
|
1525
|
-
"comando sugerido ou rode crow-skills setup --yes.",
|
|
1526
|
-
# --dest, nunca --agents codex: esses nomes, no diretório do Codex, são links do Uniflow, e
|
|
1527
|
-
# o uninstall ali recusaria com unsafe_destination.
|
|
1528
|
-
"suggested_command": ("crow-skills uninstall " + " ".join(codex_view["duplicates"]) + " --dest "
|
|
1529
|
-
+ str(legacy_shared_dir(home)) + " --yes") if codex_view["duplicates"] else None}
|
|
1530
1390
|
if plan["items"] or (gplan and gplan["items"]) or ui.get("changes"):
|
|
1531
1391
|
authorize(args, interactive, summary)
|
|
1532
1392
|
txid = None
|
|
@@ -1568,17 +1428,12 @@ def render_status(data, doctor=False, stream=None):
|
|
|
1568
1428
|
by_target = {}
|
|
1569
1429
|
for row in data["installations"]:
|
|
1570
1430
|
item = by_target.setdefault(row["target"], {"agents": row["agents"], "ok": 0, "via": 0, "bad": [], "kept": []})
|
|
1571
|
-
if row
|
|
1572
|
-
item["kept"].append(row["name"] + " link do Uniflow não gerenciado (crow-skills setup --agents uniflow --replace --yes)")
|
|
1573
|
-
if row["status"] == "uniflow_link":
|
|
1574
|
-
item["ok"] += 1
|
|
1575
|
-
item["via"] += 1 # projected by Uniflow from its store: installed for this CLI
|
|
1576
|
-
elif row["status"] in ("installed", "disabled"):
|
|
1431
|
+
if row["status"] == "installed":
|
|
1577
1432
|
item["ok"] += 1
|
|
1578
1433
|
elif row["status"] in ("modified", "missing", "unsafe", "external_link"):
|
|
1579
1434
|
item["bad"].append((row["name"] or "raiz") + " " + row["status"])
|
|
1580
1435
|
rows = [[ui.mark("fail" if v["bad"] else ("warn" if v["kept"] else "ok"), stream), ",".join(v["agents"]),
|
|
1581
|
-
str(v["ok"]) + " skills"
|
|
1436
|
+
str(v["ok"]) + " skills",
|
|
1582
1437
|
"; ".join(v["bad"] + v["kept"])] for v in by_target.values() if v["ok"] or v["bad"] or v["kept"]]
|
|
1583
1438
|
if rows:
|
|
1584
1439
|
lines += ui.table(rows, ["", "agente", "instaladas", "problemas"], stream)
|
package/crow_skills/guide.py
CHANGED
|
@@ -24,7 +24,7 @@ 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
|
|
27
|
+
from .agents import destinations
|
|
28
28
|
from .source import NAME, CrowError, SHA
|
|
29
29
|
from .storage import atomic_json, checked, private_dir, read_json
|
|
30
30
|
|
|
@@ -32,7 +32,7 @@ HEADER = "<!-- Gerenciado por crow-skills guide. Não edite; use crow-skills gui
|
|
|
32
32
|
BEGIN = "# >>> crow-skills guide (gerenciado; use crow-skills guide uninstall) >>>"
|
|
33
33
|
END = "# <<< crow-skills guide <<<"
|
|
34
34
|
TOML_KEY = "developer_instructions"
|
|
35
|
-
PASSIVE_GUIDE_SKILLS = frozenset({"crow-behaviour", "crow-uny-mcp", "crow-tree-clean-up"})
|
|
35
|
+
PASSIVE_GUIDE_SKILLS = frozenset({"crow-behaviour", "crow-model-level", "crow-uny-mcp", "crow-tree-clean-up"})
|
|
36
36
|
MAX_CONFIG = 4 * 1024 * 1024
|
|
37
37
|
HEX = re.compile(r"[a-f0-9]{64}\Z")
|
|
38
38
|
PRESERVED = re.compile(r"[a-f0-9]{32}/[a-z]+\.bak\Z")
|
|
@@ -52,12 +52,10 @@ MECHANISMS = {
|
|
|
52
52
|
"label": "Devin CLI", "invoke": "/devin:{name}",
|
|
53
53
|
"load": "use a ferramenta de skills do Devin ou digite /devin:nome"},
|
|
54
54
|
}
|
|
55
|
-
UNVERIFIED = {
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
# Shared prompt files (often symlinks into ~/.config/uniflow) and their targets.
|
|
55
|
+
UNVERIFIED = {}
|
|
56
|
+
# Shared prompt files and their targets remain protected regardless of origin.
|
|
59
57
|
PROTECTED = (".claude/CLAUDE.md", ".codex/AGENTS.md", ".codex/AGENTS.override.md", ".gemini/GEMINI.md",
|
|
60
|
-
".config/devin/CLAUDE.md", ".config/devin/AGENTS.md"
|
|
58
|
+
".config/devin/CLAUDE.md", ".config/devin/AGENTS.md")
|
|
61
59
|
|
|
62
60
|
|
|
63
61
|
def sha(data):
|
|
@@ -88,14 +86,13 @@ def select(agents):
|
|
|
88
86
|
return result
|
|
89
87
|
|
|
90
88
|
|
|
91
|
-
UNIFLOW_SKILLS = "Somente ~/.config/uniflow/skills/crow-* (o store de skills do Uniflow) é gravável: "
|
|
92
|
-
|
|
93
|
-
|
|
94
89
|
def inside(path, guard):
|
|
95
90
|
"""path is guard or below it: textually, or by file identity for an existing guard,
|
|
96
91
|
so a case-insensitive filesystem (APFS) or another spelling does not bypass it."""
|
|
97
92
|
if path == guard or path.is_relative_to(guard):
|
|
98
93
|
return True
|
|
94
|
+
if guard.is_symlink():
|
|
95
|
+
return False # Do not stat a shared-prompt symlink target merely to inspect another path.
|
|
99
96
|
if not os.path.exists(guard):
|
|
100
97
|
return False
|
|
101
98
|
for part in (path, *path.parents):
|
|
@@ -108,13 +105,19 @@ def inside(path, guard):
|
|
|
108
105
|
|
|
109
106
|
|
|
110
107
|
def shielded(path, home):
|
|
111
|
-
"""True when path is a shared prompt
|
|
108
|
+
"""True when path is a shared prompt or inside its target."""
|
|
112
109
|
path = Path(os.path.abspath(path))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
from .agents import physical
|
|
111
|
+
for guard in [Path(os.path.abspath(home / rel)) for rel in PROTECTED]:
|
|
112
|
+
if path == guard or path.is_relative_to(guard):
|
|
113
|
+
return True
|
|
114
|
+
try:
|
|
115
|
+
physical_guard = Path(physical(guard))
|
|
116
|
+
except CrowError as exc:
|
|
117
|
+
if exc.code == "unsupported_destination":
|
|
118
|
+
continue # Even lstat(guard) could traverse a retired ancestor.
|
|
119
|
+
raise
|
|
120
|
+
if any(inside(path, item) for item in {guard, physical_guard}):
|
|
118
121
|
return True
|
|
119
122
|
return False
|
|
120
123
|
|
|
@@ -136,19 +139,10 @@ def protect(path, home):
|
|
|
136
139
|
|
|
137
140
|
|
|
138
141
|
def protect_skill(path, home):
|
|
139
|
-
"""Skill
|
|
140
|
-
inside the Uniflow store. The store's other entries and the rest of the Uniflow
|
|
141
|
-
config (config.json, prompts, backups) stay protected. The package entry itself may
|
|
142
|
-
be a link Uniflow made; the plan decides about it, never following it."""
|
|
143
|
-
path = Path(os.path.abspath(path))
|
|
144
|
-
from .agents import physical
|
|
145
|
-
# Mesmo motivo: o store em dotfiles resolve, e a comparação crua nunca batia.
|
|
146
|
-
if physical(path.parent) == physical(uniflow_store(home)) and NAME.fullmatch(path.name):
|
|
147
|
-
checked(path.parent)
|
|
148
|
-
return path
|
|
142
|
+
"""Skill entries never follow symlinks; shared prompt targets stay protected."""
|
|
149
143
|
path = checked(path)
|
|
150
144
|
if shielded(path, home):
|
|
151
|
-
raise CrowError("protected_path",
|
|
145
|
+
raise CrowError("protected_path", "Prompt compartilhado ou alvo protegido: " + str(path))
|
|
152
146
|
return path
|
|
153
147
|
|
|
154
148
|
|
|
@@ -192,44 +186,9 @@ def clean(text):
|
|
|
192
186
|
|
|
193
187
|
|
|
194
188
|
def managed_skills(agent, home, store):
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
rows = store.status([target], ())
|
|
199
|
-
store_ok = store_managed(home, store) if any(r["status"] == "uniflow_link" for r in rows) else set()
|
|
200
|
-
names = {r["name"] for r in rows if r["status"] in ("installed", "modified")
|
|
201
|
-
or (r["status"] == "uniflow_link" and r["name"] in store_ok)}
|
|
202
|
-
from .projection import projected
|
|
203
|
-
return sorted(names | projected(agent, home, store)) # codex: Uniflow's links in ~/.codex/skills
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def store_statuses(home, store):
|
|
207
|
-
"""{name: status} of the Uniflow store (crow-skills view); {} when it cannot be read."""
|
|
208
|
-
try:
|
|
209
|
-
return {r["name"]: r["status"] for r in store.status(destinations("uniflow", home), ()) if r["name"]}
|
|
210
|
-
except (CrowError, OSError):
|
|
211
|
-
return {}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def store_managed(home, store):
|
|
215
|
-
"""Names whose Uniflow store copy is an intact crow-skills install. A link to a store entry the user wrote
|
|
216
|
-
by hand, or to a managed entry that was edited or lost files (a CLI may fail to load it), does not count
|
|
217
|
-
as an install for the CLI."""
|
|
218
|
-
return {n for n, s in store_statuses(home, store).items() if s == "installed"}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def store_broken(home, store):
|
|
222
|
-
"""Managed store entries that were edited or lost files (status modified/missing)."""
|
|
223
|
-
return {n for n, s in store_statuses(home, store).items() if s in ("modified", "missing")}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
BROKEN_REASON = ("a entrada do store do Uniflow foi editada ou perdeu arquivos; o CLI pode não carregá-la, então não conta "
|
|
227
|
-
"como instalada")
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def broken_fix(name):
|
|
231
|
-
return ("revise o store e, para restaurá-lo com backup: crow-skills update " + name +
|
|
232
|
-
" --agents uniflow --global --overwrite-modified --yes")
|
|
189
|
+
"""Only locally managed native packages count; individual links are unsafe."""
|
|
190
|
+
rows = store.status(destinations(agent, home), ())
|
|
191
|
+
return sorted(r["name"] for r in rows if r["status"] in ("installed", "modified"))
|
|
233
192
|
|
|
234
193
|
|
|
235
194
|
def present_skills(agent, home, store, catalog):
|
|
@@ -271,6 +230,15 @@ def render_body(agent, catalog, revision, skills):
|
|
|
271
230
|
]
|
|
272
231
|
if "crow-behaviour" not in skills:
|
|
273
232
|
lines = lines[:4] + ["## Skills especialistas"]
|
|
233
|
+
if "crow-model-level" in skills:
|
|
234
|
+
lines[-1:-1] = [
|
|
235
|
+
"## crow-model-level: nível por unidade, obrigatório ao delegar",
|
|
236
|
+
"- Carregue " + invoke(name="crow-model-level") + " antes de enviar qualquer delegação, inclusive uma só, e independentemente do modo de orquestração crow-behaviour ON/OFF. É ela que decide o nível, e é a instrução explícita que os hosts exigem para que o esforço de raciocínio possa ser definido em vez de herdado.",
|
|
237
|
+
"- Cada unidade delegada sai com o nível declarado: modelo e esforço. Campo que o host não expõe, ou que o modo de delegação ignora, entra como ausente e com o motivo, em vez de sumir da linha.",
|
|
238
|
+
"- O esforço vem do sinal da unidade, não de uma regra única: braçal — resultado que você especifica e aceita pela evidência sem ver o caminho — pede esforço mínimo e o modelo mais barato que dá conta; unidade que exige decidir no caminho (causa-raiz, ameaça, desenho, requisito contraditório) pede esforço alto e o modelo mais forte, mesmo sendo pequena.",
|
|
239
|
+
"- Instrução do dono sobre modelo ou esforço é o nível e prevalece sobre o sinal lido. Nível alto não aumenta concorrência: o teto de RAM/CPU e o número de agentes continuam sendo os da crow-behaviour.",
|
|
240
|
+
"",
|
|
241
|
+
]
|
|
274
242
|
if "crow-uny-mcp" in skills:
|
|
275
243
|
lines[-1:-1] = [
|
|
276
244
|
"## crow-uny-mcp: gerenciamento passivo obrigatório",
|