@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.
@@ -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, refuse_store_as_cli_dir)
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, Uniflow, tmux) e "
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
- # config do Uniflow), que `guide.protect()` recusa com protected_path: aquele agente entra em
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", "disabled", "uniflow_link"):
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
- # A mesma correção que status/doctor aplicam: projeção do Uniflow quebrada, desabilitada ou para
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", []) + entry.get("via_uniflow", []))
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 = unmanaged_projections(store, home, store.status(targets))
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
- rows[:] = projection.status_rows(home, store, rows) # enriquece no lugar; += contaria duas vezes
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
- # Sem Uniflow nada disso duplica uma projeção, então o aviso acima nunca apareceria
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
- codex_view = None
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.get("projection") == "uniflow_link_unmanaged":
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" + (" (" + str(v["via"]) + " via uniflow)" if v["via"] else ""),
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)
@@ -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, uniflow_guards, uniflow_store
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
- "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.",
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", ".config/uniflow")
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, Uniflow's config (as Uniflow resolves it) or inside their targets."""
108
+ """True when path is a shared prompt or inside its target."""
112
109
  path = Path(os.path.abspath(path))
113
- for guard in [Path(os.path.abspath(home / rel)) for rel in PROTECTED] + uniflow_guards(home):
114
- guards = {guard}
115
- if os.path.lexists(guard):
116
- guards.add(Path(os.path.realpath(guard)))
117
- if any(inside(path, item) for item in guards):
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 packages follow protect(), with one exception: a crow-* package directly
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", UNIFLOW_SKILLS + str(path) + " é protegido.")
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
- """crow-* packages available to the agent: the ones this CLI manages there (edited copies still count) and
196
- the ones Uniflow projects into its skills dir from the store (managed with --agents uniflow)."""
197
- target = destinations(agent, home)[0]
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",