@andespindola/crow-skills 0.14.0 → 0.15.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.
@@ -1,3 +1,3 @@
1
1
  """Portable, local-first installer for the crow-skills catalogue."""
2
2
 
3
- __version__ = "0.14.0"
3
+ __version__ = "0.15.0"
@@ -4,18 +4,47 @@ import os
4
4
  import shutil
5
5
  from .source import CrowError
6
6
 
7
+ # Every destination is the CLI's OWN skills directory. There is no shared folder: the
8
+ # ~/.agents/skills store that codex used until 0.14.0 made crow-skills act on a directory
9
+ # instead of on an agent, which showed up as a phantom "codex,shared" agent and let Codex
10
+ # list the same skill twice (its own dir plus the shared one). LEGACY_SHARED stays named
11
+ # only so an install made there can be found and migrated away.
12
+ LEGACY_SHARED = ".agents/skills"
7
13
  AGENTS = {
8
- "codex": (".agents/skills", ".agents/skills", "codex"),
14
+ "codex": (".codex/skills", ".codex/skills", "codex"),
9
15
  "claude": (".claude/skills", ".claude/skills", "claude"),
10
16
  "gemini": (".gemini/skills", ".gemini/skills", "gemini"),
11
17
  "devin": (".config/devin/skills", ".devin/skills", "devin"),
12
- "shared": (".agents/skills", ".agents/skills", None),
13
18
  # Uniflow keeps one store and links it into each CLI's user skills dir; it has
14
19
  # no project scope. The store sits beside config.json (see uniflow_store).
15
20
  "uniflow": (".config/uniflow/skills", None, "uniflow"),
16
21
  }
17
22
 
18
23
 
24
+ def codex_home(home):
25
+ """$CODEX_HOME for the real profile (as Codex resolves it); an isolated --home uses <home>/.codex.
26
+
27
+ A RELATIVE $CODEX_HOME is ignored: os.path.abspath would resolve it against this process's cwd,
28
+ which is not Codex's, so the destination would depend on where the command was run and could
29
+ land inside the user's repository. Only an absolute (or ~-anchored) value is honoured."""
30
+ home = Path(home)
31
+ raw = os.environ.get("CODEX_HOME") if real_home(home) else None
32
+ if raw:
33
+ expanded = Path(raw).expanduser()
34
+ if expanded.is_absolute():
35
+ return Path(os.path.abspath(expanded))
36
+ return Path(os.path.abspath(home / ".codex"))
37
+
38
+
39
+ def codex_skills_dir(home):
40
+ return codex_home(home) / "skills"
41
+
42
+
43
+ def legacy_shared_dir(home):
44
+ """Where codex installs landed until 0.14.0; read only, to migrate what is still there."""
45
+ return Path(os.path.abspath(Path(home) / LEGACY_SHARED))
46
+
47
+
19
48
  def real_home(home):
20
49
  """Environment overrides describe the real profile; an isolated --home ignores them."""
21
50
  return home == Path.home().resolve()
@@ -74,6 +103,10 @@ def destinations(names, home, project=None, custom=None):
74
103
  # Respect explicit XDG config, but isolate it when --home selects another profile.
75
104
  if name == "devin" and not project and real_home(home) and os.getenv("XDG_CONFIG_HOME"):
76
105
  path = Path(os.environ["XDG_CONFIG_HOME"]).expanduser() / "devin/skills"
106
+ # Codex resolves its own home through $CODEX_HOME; the destination must follow it, or an
107
+ # install would write beside a profile Codex never reads.
108
+ if name == "codex" and not project:
109
+ path = codex_skills_dir(home)
77
110
  if name == "uniflow":
78
111
  path = uniflow_store(home)
79
112
  key = str(path.absolute())
@@ -29,7 +29,7 @@ import subprocess
29
29
  import sys
30
30
 
31
31
  from . import __version__
32
- from .agents import destinations
32
+ from .agents import destinations, legacy_shared_dir
33
33
  from .guide import protect, read_regular, sha, write_atomic
34
34
  from .source import CrowError, Source, same_source
35
35
  from .storage import atomic_json, checked, private_dir, read_json
@@ -334,8 +334,9 @@ def reinstall_behaviour(home, entry, dry_run, trust):
334
334
 
335
335
 
336
336
  def codex_duplicates(home, store, entry, names, dry_run):
337
- """Codex sees Uniflow's links in ~/.codex/skills: those names are not updated natively, and intact managed
338
- native copies of them are removed through uninstall (backup, transaction). Returns the names to update."""
337
+ """Codex sees Uniflow's links in its own dir: those names are not updated there, and intact managed copies
338
+ left in the legacy shared folder are removed through uninstall against THAT folder (backup, transaction).
339
+ Returns the names to update."""
339
340
  from . import projection
340
341
  from .cli import parser, run as cli_run
341
342
  seen = projection.projected("codex", home, store)
@@ -347,7 +348,8 @@ def codex_duplicates(home, store, entry, names, dry_run):
347
348
  entry["kept"] += [{"name": n, "status": "duplicate_modified"} for n in dupes["modified"]]
348
349
  entry["kept"] += [{"name": n, "status": "duplicate_shared"} for n in dupes["shared"]]
349
350
  if dupes["remove"]:
350
- argv = ["uninstall", *dupes["remove"], "--agents", "codex", "--global", "--json", "--home", str(home),
351
+ # --dest na pasta legada: em ~/.codex/skills esses nomes são links do Uniflow e o uninstall recusaria.
352
+ argv = ["uninstall", *dupes["remove"], "--dest", str(legacy_shared_dir(home)), "--json", "--home", str(home),
351
353
  "--dry-run" if dry_run else "--yes", "--no-integrations"]
352
354
  try:
353
355
  data = cli_run(parser().parse_args(argv))
@@ -411,6 +413,11 @@ def skills_update(home, trust=True, dry_run=False):
411
413
  entry["error"] = {"code": exc.code, "message": str(exc)}
412
414
  except (OSError, ValueError, KeyError, TypeError):
413
415
  entry["error"] = {"code": "io_error", "message": "Falha de leitura/gravação; nada concluído para este agente."}
416
+ if agent == "codex" and not entry.get("error"):
417
+ # Ramo mais provável de quem vinha da 0.14.0: no diretório do Codex só há links do
418
+ # Uniflow, e TODAS as cópias do codex estão na pasta compartilhada. Sem migrar aqui,
419
+ # uma máquina que só roda autoupdate nunca sai daquela pasta.
420
+ machine.migrate_legacy(None, home, store, entry, dry_run, planned=projected)
414
421
  results.append(entry)
415
422
  continue
416
423
  records = store.state(target["path"])["installed"]
@@ -455,6 +462,12 @@ def skills_update(home, trust=True, dry_run=False):
455
462
  entry["error"] = {"code": exc.code, "message": str(exc)}
456
463
  except (OSError, ValueError, KeyError, TypeError):
457
464
  entry["error"] = {"code": "io_error", "message": "Falha de leitura/gravação; nada concluído para este agente."}
465
+ if agent == "codex" and not entry.get("error"):
466
+ # Mesma migração do setup, e pelo mesmo motivo: sem isto, uma máquina que só roda
467
+ # autoupdate/upgrade mantém a pasta compartilhada antiga para sempre. Depois da
468
+ # atualização, nunca antes.
469
+ from . import machine
470
+ machine.migrate_legacy(None, home, store, entry, dry_run)
458
471
  results.append(entry)
459
472
  return results
460
473
 
@@ -18,7 +18,7 @@ from . import projection
18
18
  from . import resources
19
19
  from . import machine
20
20
  from . import ui
21
- from .agents import AGENTS, destinations, inventory
21
+ from .agents import AGENTS, destinations, inventory, legacy_shared_dir
22
22
  from .source import CrowError, Source, valid_name
23
23
  from .storage import Store, checked
24
24
 
@@ -166,6 +166,12 @@ def parser():
166
166
  help="Não aplicar/remover as integrações de interface (status line, tmux) em --global")
167
167
  if name != "uninstall":
168
168
  child.add_argument("--no-trust", action="store_true", help=TRUST_HELP)
169
+ if name == "uninstall":
170
+ # Interno: a migração da pasta compartilhada roda como um uninstall logo depois de um
171
+ # install, e precisa ficar atrelada a ele para que um rollback do install desfaça os dois.
172
+ child.add_argument("--linked-to", dest="linked_to", metavar="TXID", default=argparse.SUPPRESS,
173
+ help=argparse.SUPPRESS) # SUPPRESS: ausente não cria atributo, então
174
+ # uma invocação existente continua parseando com exatamente o mesmo conjunto de atributos.
169
175
  child = command_parser(sub, "guide", [common])
170
176
  child.add_argument("action", choices=("install", "status", "uninstall"))
171
177
  child.add_argument("--replace", action="store_true", help="Autorizar backup e substituição de conteúdo não gerenciado no caminho do guia")
@@ -367,9 +373,9 @@ def guide_sync(args, home, store, command, targets, plan, source):
367
373
  return report, gplan
368
374
 
369
375
 
370
- def apply_chain(store, plan, gplan, home):
376
+ def apply_chain(store, plan, gplan, home, linked=None):
371
377
  """Skills first, then the guide; a failed guide rolls the skills back."""
372
- txid = store.apply(plan)
378
+ txid = store.apply(plan, linked=linked)
373
379
  if gplan is None:
374
380
  return txid, None
375
381
  try:
@@ -716,24 +722,31 @@ def resync_integrations(home, store):
716
722
  pass # Reported by status/doctor; never turns a completed rollback into a failure.
717
723
 
718
724
 
725
+ def linked_transactions(store, txid):
726
+ """Committed transactions that COMPLETE the given one and must be undone before it: the guide,
727
+ and the uninstall that migrated the legacy shared folder after an install."""
728
+ return [t["id"] for t in store.transactions()
729
+ if t.get("linked") == txid and t["phase"] == "committed"]
730
+
731
+
719
732
  def linked_guides(store, txid):
720
733
  """Committed guide transactions that completed the given skills transaction."""
721
734
  return [t["id"] for t in store.transactions() if t.get("kind") == "guide" and t.get("linked") == txid and t["phase"] == "committed"]
722
735
 
723
736
 
724
737
  # What a native copy means once Uniflow links its store into each CLI's user skills dir.
725
- # Uniflow links codex into ~/.codex/skills, not into ~/.agents/skills (this CLI's codex root).
738
+ # Every CLI, codex included since 0.15.0, has its skills installed in its OWN dir, the same one
739
+ # Uniflow links into, so a native copy of a name always just wins there (no second directory).
726
740
  NATIVE_EFFECT = {
727
741
  "claude": "skipped", "gemini": "skipped", "devin": "skipped",
728
- "codex": "both",
742
+ "codex": "skipped",
729
743
  }
730
744
 
731
745
 
732
746
  def uniflow_notice(home, store, targets, plan):
733
747
  """Installing into the Uniflow store while native crow-* copies remain. Uniflow does
734
748
  not link a name a CLI already has in the same dir, so that copy (possibly another
735
- revision) wins there; codex reads ~/.agents/skills and Uniflow's ~/.codex/skills
736
- link, so it sees both. Report it and the explicit command; never remove anything."""
749
+ revision) wins there. Report it and the explicit command; never remove anything."""
737
750
  if not any("uniflow" in t["agents"] for t in targets):
738
751
  return None
739
752
  names = sorted({c["name"] for c in plan["changes"] if "uniflow" in c["agents"]})
@@ -765,8 +778,9 @@ def uniflow_notice(home, store, targets, plan):
765
778
  for names_, agents_ in groups.items()]
766
779
  return {"native_duplicates": list(native.values()), "uninspected": uninspected,
767
780
  "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 "
768
- "store. effect=both: o Uniflow liga o store em ~/.codex/skills e o Codex também lê ~/.agents/skills, então vê as "
769
- "duas cópias. Nada foi removido. Remover a cópia nativa de crow-behaviour remove também o guia daquele agente.",
781
+ "store. Desde a 0.15.0 nenhum destino produz effect=both: todo CLI instala no diretório dele, o mesmo em que o "
782
+ "Uniflow projeta o store, então não há uma segunda raiz em que o mesmo nome apareça de novo. Nada foi removido. "
783
+ "Remover a cópia nativa de crow-behaviour remove também o guia daquele agente.",
770
784
  "suggested_commands": commands, "suggested_command": " && ".join(commands) if commands else None}
771
785
 
772
786
 
@@ -803,7 +817,7 @@ def codex_projection_split(home, store, targets, names):
803
817
  """The user-scope codex target and which of the names Codex already sees via Uniflow (install skips them)."""
804
818
  target = next((t for t in targets if projection.native_target("codex", t, home)), None)
805
819
  if target is None or set(target["agents"]) - {"codex"}:
806
- return None # --agents shared: other hosts read that dir without Uniflow's codex projection
820
+ return None # a dir shared with another agent is not Codex's own projected dir
807
821
  projected = projection.projected("codex", home, store)
808
822
  if not projected:
809
823
  return None
@@ -946,16 +960,30 @@ def run(args):
946
960
  "mode": hook.read_mode(home), "activation_verified": False,
947
961
  "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
948
962
  if any(projection.native_target("codex", t, home) for t in targets):
949
- rows += projection.status_rows(home, store, rows)
963
+ rows[:] = projection.status_rows(home, store, rows) # enriquece no lugar; += contaria duas vezes
950
964
  dupes = projection.duplicates(home, store)
951
965
  names_ = dupes["remove"] + dupes["modified"] + dupes["shared"]
952
966
  if names_:
953
967
  # A warning, not unhealthy: both copies are intact and work; Codex just lists them twice.
954
- result["warnings"] = [{"code": "codex_duplicates", "skills": names_,
955
- "message": "O Codex vê " + str(len(names_)) + " skill(s) duas vezes (cópia em ~/.agents/skills "
956
- "e link do Uniflow em " + str(projection.codex_skills_dir(home)) + ").",
957
- "fix": "crow-skills setup --agents codex --yes"
958
- + (" --overwrite-modified" if dupes["modified"] else "")}]
968
+ result.setdefault("warnings", []).append({"code": "codex_duplicates", "skills": names_,
969
+ "message": "O Codex vê " + str(len(names_)) + " skill(s) duas vezes: cópia antiga na pasta "
970
+ "compartilhada " + str(legacy_shared_dir(home)) + " e a instalação em "
971
+ + str(projection.codex_skills_dir(home)) + ". O crow-skills não usa mais a pasta "
972
+ "compartilhada; remover as cópias de lá encerra a duplicação.",
973
+ "fix": "crow-skills uninstall " + ",".join(names_) + " --dest "
974
+ + str(legacy_shared_dir(home)) + " --yes"
975
+ + (" --overwrite-modified" if dupes["modified"] else "")})
976
+ leftovers = [n for n in projection.legacy_leftovers(home, store) if n not in names_]
977
+ if leftovers:
978
+ # Sem Uniflow nada disso duplica uma projeção, então o aviso acima nunca apareceria
979
+ # e uma instalação feita pela 0.14.0 ficaria invisível aqui, ainda carregada pelo Codex.
980
+ result.setdefault("warnings", []).append(
981
+ {"code": "codex_legacy_shared", "skills": leftovers,
982
+ "message": "O crow-skills ainda tem " + str(len(leftovers)) + " skill(s) instalada(s) na pasta "
983
+ "compartilhada antiga " + str(legacy_shared_dir(home)) + ", que esta versão não usa mais; o "
984
+ "Codex continua carregando de lá.",
985
+ "fix": "crow-skills setup --agents codex --yes (instala no diretório do Codex e migra), ou "
986
+ "crow-skills uninstall " + ",".join(leftovers) + " --dest " + str(legacy_shared_dir(home)) + " --yes"})
959
987
  result["groups"] = group_coverage(rows)
960
988
  try:
961
989
  result["autoupdate"] = autoupdate.status(store, home) if global_view else None
@@ -983,7 +1011,7 @@ def run(args):
983
1011
  # on recover, an interrupted guide also undoes the skills it completed.
984
1012
  follow = []
985
1013
  if args.command == "rollback":
986
- before_ids = [g for txid in ids for g in linked_guides(store, txid)]
1014
+ before_ids = [g for txid in ids for g in linked_transactions(store, txid)]
987
1015
  ids, transactions = before_ids + ids, [store.journal(t) for t in before_ids] + transactions
988
1016
  else:
989
1017
  follow = [t["linked"] for t in transactions if t.get("linked")
@@ -1064,15 +1092,19 @@ def run(args):
1064
1092
  summary["codex_projection"] = {
1065
1093
  "via_uniflow": sorted(codex_view["skip"]), "duplicates": codex_view["duplicates"],
1066
1094
  "note": "O Codex já vê estas skills pelo link do Uniflow em " + str(projection.codex_skills_dir(home)) +
1067
- "; nenhuma cópia nativa é criada para o codex. Cópias nativas que duplicam a projeção não são removidas "
1068
- "por install; remova-as com o comando sugerido ou rode crow-skills setup --yes.",
1069
- "suggested_command": ("crow-skills uninstall " + " ".join(codex_view["duplicates"]) + " --agents codex --global")
1070
- if codex_view["duplicates"] else None}
1095
+ "; nenhuma cópia é criada para o codex. As cópias que ficaram na pasta compartilhada antiga "
1096
+ + str(legacy_shared_dir(home)) + " duplicam a projeção e não são removidas por install; remova-as com o "
1097
+ "comando sugerido ou rode crow-skills setup --yes.",
1098
+ # --dest, nunca --agents codex: esses nomes, no diretório do Codex, são links do Uniflow, e
1099
+ # o uninstall ali recusaria com unsafe_destination.
1100
+ "suggested_command": ("crow-skills uninstall " + " ".join(codex_view["duplicates"]) + " --dest "
1101
+ + str(legacy_shared_dir(home)) + " --yes") if codex_view["duplicates"] else None}
1071
1102
  if plan["items"] or (gplan and gplan["items"]) or ui.get("changes"):
1072
1103
  authorize(args, interactive, summary)
1073
1104
  txid = None
1074
1105
  if not getattr(args, "dry_run", False):
1075
- txid, guides["transaction"] = apply_chain(store, plan, gplan, home)
1106
+ txid, guides["transaction"] = apply_chain(store, plan, gplan, home,
1107
+ linked=getattr(args, "linked_to", None))
1076
1108
  if not ui.get("disabled"):
1077
1109
  # Best effort after the skills/guide transactions; a refusal is reported, never undoes them.
1078
1110
  try:
@@ -52,7 +52,6 @@ MECHANISMS = {
52
52
  "load": "use a ferramenta de skills do Devin ou digite /devin:nome"},
53
53
  }
54
54
  UNVERIFIED = {
55
- "shared": "shared é um diretório de skills, não um CLI; não há onde gravar instruções.",
56
55
  "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
56
  }
58
57
  # Shared prompt files (often symlinks into ~/.config/uniflow) and their targets.
@@ -239,13 +239,20 @@ def broadcast_module():
239
239
  return _BROADCAST or None
240
240
 
241
241
 
242
- def resource_hint(home):
243
- """The one-line memory hint, or None (no pressure, no probe, or any failure)."""
242
+ def resource_hint(home, write_through=True):
243
+ """The one-line memory hint, or None (no pressure, no probe, or any failure).
244
+
245
+ write_through=False for `--probe`: the reading is still taken, but a cache miss must not write
246
+ resources-cache.json, or "exercise the hook without writing anything" would be false."""
244
247
  module = resources_module()
245
248
  if module is None:
246
249
  return None
247
250
  try:
248
- text = module.hint(module.probe(home, processes=False, use_cache=True))
251
+ try:
252
+ reading = module.probe(home, processes=False, use_cache=True, write_through=write_through)
253
+ except TypeError: # crow_resources.py copy older than this hook
254
+ reading = module.probe(home, processes=False, use_cache=True)
255
+ text = module.hint(reading)
249
256
  if text and hasattr(module, "orphan_hint"):
250
257
  text += module.orphan_hint(home) # last orphan scan, a file read: no ps call per prompt
251
258
  return text
@@ -705,7 +712,7 @@ def check_only(host, raw, payload, home):
705
712
  return broadcast_check(host, raw, home, module, me, touched, tree)
706
713
 
707
714
 
708
- def handle(host, raw, payload, home, env=None, check=False):
715
+ def handle(host, raw, payload, home, env=None, check=False, probe=False):
709
716
  """Process one hook call; returns the JSON to print (or None). Never raises to the caller."""
710
717
  env = os.environ if env is None else env
711
718
  if host not in HOSTS or not isinstance(payload, dict) or foreign(host, payload, env, home):
@@ -714,7 +721,7 @@ def handle(host, raw, payload, home, env=None, check=False):
714
721
  return check_only(host, raw, payload, home)
715
722
  event = normalise(host, raw, payload)
716
723
  current = read_mode(home)
717
- if raw in PROMPT_EVENTS:
724
+ if raw in PROMPT_EVENTS and not probe:
718
725
  text = prompt_text(payload)
719
726
  match = COMMAND.match(text) if text and len(text) < 64 else None
720
727
  if match and host in ("codex", "devin", "agy"):
@@ -756,8 +763,17 @@ def handle(host, raw, payload, home, env=None, check=False):
756
763
  module = None
757
764
  except Exception: # noqa: BLE001 - the broadcast never breaks the event
758
765
  module, presence = None, None
759
- stamped, snap = record(home, dict(event, mode=current["mode"]), snapshot=True, fallback_cwd=process_cwd(), presence=presence)
760
- if raw in STOP_EVENTS:
766
+ if probe:
767
+ # Everything a real event would compute, nothing it would write: no event appended, no session
768
+ # snapshot, no presence. Validating that a CLI's hook is wired must never invent a live session.
769
+ stamped = dict(event, mode=current["mode"])
770
+ snap = {"key": session_key(host, event.get("session"), event.get("cwd")), "host": host,
771
+ "session": event.get("session"), "cwd": event.get("cwd") or process_cwd(),
772
+ "state": "working", "presence": {}}
773
+ else:
774
+ stamped, snap = record(home, dict(event, mode=current["mode"]), snapshot=True, fallback_cwd=process_cwd(),
775
+ presence=presence)
776
+ if raw in STOP_EVENTS and not probe:
761
777
  cli = record_processes(home, host, event)
762
778
  if cli and snap is not None:
763
779
  bmod = broadcast_module()
@@ -786,12 +802,12 @@ def handle(host, raw, payload, home, env=None, check=False):
786
802
  stamp = current["mode"] + "@" + str(current["updatedAt"])
787
803
  if host in ("codex", "gemini", "devin") and (raw == "SessionStart" or (seen and seen != stamp)):
788
804
  system = indicator(current["mode"]) # visible line only at start or when the mode changed
789
- if snap is not None and seen != stamp:
805
+ if snap is not None and seen != stamp and not probe:
790
806
  update_session(home, dict(stamped, event="other"), {"modeSeen": stamp, "mode": current["mode"]})
791
807
  if raw == "SessionStart":
792
808
  return output_for(host, raw, system=system) if system else None
793
809
  context = context_line(current["mode"])
794
- memory = resource_hint(home)
810
+ memory = resource_hint(home, write_through=not probe)
795
811
  if memory:
796
812
  context += "\n" + memory
797
813
  if module is not None and snap is not None:
@@ -854,8 +870,8 @@ def fast_hook_args(argv):
854
870
  if name in ("--host", "--event", "--home") and i + 1 < len(argv):
855
871
  values[name[2:]] = argv[i + 1]
856
872
  i += 2
857
- elif name == "--check":
858
- flags.add("check")
873
+ elif name in ("--check", "--probe"):
874
+ flags.add(name[2:])
859
875
  i += 1
860
876
  else:
861
877
  return None
@@ -863,7 +879,7 @@ def fast_hook_args(argv):
863
879
  return None
864
880
  import types
865
881
  return types.SimpleNamespace(command="hook", host=values["host"], event=values["event"], home=values.get("home"),
866
- check="check" in flags)
882
+ check="check" in flags, probe="probe" in flags)
867
883
 
868
884
 
869
885
  def main(argv=None):
@@ -882,6 +898,8 @@ def parse_args(argv):
882
898
  hook.add_argument("--event", required=True)
883
899
  hook.add_argument("--home")
884
900
  hook.add_argument("--check", action="store_true")
901
+ # Exercise the hook without side effects: validating a CLI's wiring must not register a session.
902
+ hook.add_argument("--probe", action="store_true")
885
903
  mode = sub.add_parser("mode")
886
904
  mode.add_argument("action", nargs="?", default="status", choices=("on", "off", "status", "toggle"))
887
905
  mode.add_argument("--home")
@@ -908,11 +926,11 @@ def run_main(args):
908
926
  try:
909
927
  raw = sys.stdin.buffer.read(MAX_STDIN + 1) if not sys.stdin.isatty() else b""
910
928
  payload = json.loads(raw.decode("utf-8")) if raw.strip() and len(raw) <= MAX_STDIN else {}
911
- result = handle(args.host, args.event, payload, home, check=args.check)
929
+ result = handle(args.host, args.event, payload, home, check=args.check, probe=getattr(args, "probe", False))
912
930
  if result:
913
931
  sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
914
932
  sys.stdout.flush()
915
- if args.event == "SessionStart" and not args.check:
933
+ if args.event == "SessionStart" and not args.check and not getattr(args, "probe", False):
916
934
  prune_sessions(home)
917
935
  module = broadcast_module()
918
936
  if module is not None:
@@ -30,7 +30,7 @@ from . import hook
30
30
  from . import integrations
31
31
  from . import resources
32
32
  from . import ui
33
- from .agents import destinations, uniflow_config_dir
33
+ from .agents import destinations, legacy_shared_dir, uniflow_config_dir
34
34
  from .source import CrowError, Source
35
35
  from .storage import Store
36
36
 
@@ -251,6 +251,9 @@ def configure_agent(args, home, store, agent, names, dry_run, store_names=()):
251
251
  if not install:
252
252
  entry.update(refresh_extras(agent, home, dry_run, not getattr(args, "no_trust", False), getattr(args, "source", None),
253
253
  getattr(args, "ref", None), replace, overwrite))
254
+ if agent == "codex":
255
+ # Nada a instalar: o que vale como substituto é o que já está lá (unchanged).
256
+ migrate_legacy(args, home, store, entry, dry_run, planned=entry["unchanged"])
254
257
  return entry
255
258
  argv = ["install", *install, "--agents", agent, "--global", "--json", "--home", str(home),
256
259
  "--dry-run" if dry_run else "--yes"]
@@ -275,22 +278,139 @@ def configure_agent(args, home, store, agent, names, dry_run, store_names=()):
275
278
  entry["transaction"] = data.get("transaction")
276
279
  if data.get("uniflow"):
277
280
  entry["uniflow"] = data["uniflow"]
281
+ if agent == "codex":
282
+ # Depois da instalação no diretório do Codex, nunca antes: a cópia legada só sai quando
283
+ # existe uma instalação no lugar novo. Num dry run o disco não foi tocado, então o que este
284
+ # run instalaria conta como substituto — senão o preview mentiria sobre o apply.
285
+ migrate_legacy(args, home, store, entry, dry_run,
286
+ planned=(entry["changed"] + entry["unchanged"]) if dry_run else ())
278
287
  return entry
279
288
 
280
289
 
281
290
  DUPLICATE = "removida: duplicava a projeção do Uniflow"
282
291
 
283
292
 
293
+ def migrate_legacy(args, home, store, entry, dry_run, planned=()):
294
+ """legacy_shared_step, but a failure there never costs the agent its result.
295
+
296
+ planned: names this run installs (or already has) in the CLI's own dir. In a dry run nothing is
297
+ written there, so without this the preview would judge every name as "has no replacement" and
298
+ predict the opposite of what the apply does."""
299
+ try:
300
+ legacy_shared_step(args, home, store, entry, dry_run, planned=planned)
301
+ except (CrowError, OSError, ValueError, KeyError, TypeError) as exc:
302
+ entry["legacy_shared"] = {"state": "warn", "note": "migração da pasta compartilhada antiga não concluída: " + str(exc),
303
+ "fix": "crow-skills doctor"}
304
+
305
+
306
+ def legacy_shared_step(args, home, store, entry, dry_run, planned=()):
307
+ """Remove what crow-skills left in the legacy shared folder (~/.agents/skills), where codex
308
+ installed until 0.14.0, now that codex installs in its own dir.
309
+
310
+ duplicates() only sees names Uniflow projects, so a copy of a NON-projected skill (the
311
+ coordinator itself, typically) would otherwise stay there for ever and keep being listed by
312
+ Codex beside the real install. Only crow-skills-managed, intact copies go, through the normal
313
+ uninstall (backup, transaction, rollback); an edited copy, or one also recorded for another
314
+ agent of that dir, is kept and reported. Nothing that crow-skills did not install is touched.
315
+
316
+ A copy is removed ONLY when that name really works in Codex's own dir — an intact install of
317
+ ours, or a healthy Uniflow link. Otherwise the legacy copy is the only working skill Codex has
318
+ (a broken store entry leaves the link occupying the name in the new dir), and removing it would
319
+ take the skill away instead of migrating it.
320
+ """
321
+ legacy = legacy_shared_dir(home)
322
+ # Mesmo diretório (inclusive via symlink): não há nada para migrar e avisar seria ruído.
323
+ if os.path.realpath(legacy) == os.path.realpath(entry["target"]):
324
+ return
325
+ if legacy.is_symlink():
326
+ # Não seguimos symlink para apagar nada, mas quem tem a pasta nos dotfiles precisa saber
327
+ # que a migração não aconteceu, em vez de supor que aconteceu.
328
+ entry["legacy_shared"] = {"state": "warn", "action": "skip", "path": str(legacy),
329
+ "note": "a pasta compartilhada antiga é um symlink; nada foi tocado e a migração não "
330
+ "aconteceu. Mova o conteúdo você mesmo ou aponte --dest para o caminho físico."}
331
+ return
332
+ if not legacy.is_dir():
333
+ return
334
+ target = {"path": str(legacy), "agents": ["codex"]}
335
+ try:
336
+ records = store.state(target["path"])["installed"]
337
+ rows = {r["name"]: r for r in store.status([target], sorted(records)) if r["name"]}
338
+ except (CrowError, OSError) as exc:
339
+ entry["legacy_shared"] = {"state": "warn", "note": str(exc), "path": str(legacy)}
340
+ return
341
+ # What actually works in Codex's own dir right now: our intact install, or a healthy projection.
342
+ from . import projection
343
+ try:
344
+ own = {r["name"]: r["status"] for r in store.status([destinations("codex", home)[0]], sorted(records)) if r["name"]}
345
+ except (CrowError, OSError):
346
+ own = {}
347
+ try:
348
+ healthy = {k for k, kind in projection.links(home, store).items() if kind == "managed"}
349
+ except (CrowError, OSError):
350
+ healthy = set()
351
+ # One reason per name: a more specific one already recorded for this name (a broken projection,
352
+ # an unsafe target) explains it better than anything this step would add.
353
+ already = {k["name"] for k in entry.get("kept") or []}
354
+ remove, kept, deferred = [], [], []
355
+ for name in sorted(records):
356
+ row = rows.get(name)
357
+ if not row or row["status"] not in ("installed", "modified", "missing"):
358
+ continue # unmanaged, absent or unreadable: not ours to remove
359
+ if name in already:
360
+ deferred.append(name) # já explicado por uma razão mais específica deste mesmo nome
361
+ continue
362
+ if own.get(name) != "installed" and name not in healthy and name not in set(planned or ()):
363
+ kept.append({"name": name, "status": "legacy_shared_without_replacement",
364
+ "reason": "a cópia na pasta compartilhada antiga é a única que funciona: no diretório do Codex "
365
+ "este nome não tem instalação íntegra nem projeção saudável do Uniflow",
366
+ "fix": "conserte a instalação no diretório do Codex (por exemplo crow-skills setup --agents codex "
367
+ "--yes, ou corrija a entrada do store do Uniflow) e rode de novo"})
368
+ continue
369
+ others = set(records.get(name, {}).get("agents") or ["codex"]) - {"codex"}
370
+ if others:
371
+ kept.append({"name": name, "status": "legacy_shared_of_another_agent",
372
+ "reason": "também registrada para " + ",".join(sorted(others)) + " naquele diretório",
373
+ "fix": "crow-skills uninstall " + name + " --dest " + str(legacy) + " --yes"})
374
+ elif row["status"] == "installed":
375
+ remove.append(name)
376
+ else:
377
+ kept.append({"name": name, "status": "legacy_shared_modified",
378
+ "reason": "cópia editada na pasta compartilhada antiga",
379
+ "fix": "crow-skills uninstall " + name + " --dest " + str(legacy) + " --overwrite-modified --yes"})
380
+ entry["kept"] += kept
381
+ if not remove:
382
+ # Nunca silencioso: se ainda há cópias nossas lá, o resumo diz que a pasta foi olhada e por
383
+ # que nada saiu — senão não há como auditar uma migração que não aconteceu.
384
+ if kept or deferred:
385
+ entry["legacy_shared"] = {"state": "warn", "action": "keep", "path": str(legacy),
386
+ "kept": [k["name"] for k in kept] + deferred}
387
+ return
388
+ argv = ["uninstall", *remove, "--dest", str(legacy), "--json", "--home", str(home),
389
+ "--dry-run" if dry_run else "--yes", "--no-integrations"]
390
+ if entry.get("transaction"):
391
+ # Atrelada à transação do install: um rollback dele restaura primeiro estas cópias, em vez
392
+ # de tirar a instalação nova e deixar o usuário sem nenhuma.
393
+ argv += ["--linked-to", entry["transaction"]]
394
+ data = _cli().run(_cli().parser().parse_args(argv))
395
+ entry["legacy_shared"] = {"state": "ok", "action": "plan" if dry_run else "remove", "path": str(legacy),
396
+ "removed": [c["name"] for c in data["changes"] if c["action"] != "unchanged"],
397
+ "transaction": data.get("transaction"),
398
+ "note": "o crow-skills não usa mais a pasta compartilhada; as cópias de lá foram removidas "
399
+ "(com backup) agora que o codex instala no diretório dele"}
400
+
401
+
284
402
  def codex_projection_step(args, home, store, entry, names, install, waiting, store_names, dry_run):
285
- """Codex sees ~/.codex/skills (Uniflow's links to its store) and ~/.agents/skills (our copies) and lists a
286
- same-name skill twice. A name Codex sees via a managed projection gets no native copy, and an intact
287
- managed native copy of it is removed through uninstall (backup, transaction, rollback). Returns the
288
- names still to install natively."""
403
+ """Codex sees its own dir (where crow-skills installs and Uniflow links its store) and also the legacy
404
+ shared ~/.agents/skills, so a copy left there by a version before 0.15.0 is still listed twice. A name
405
+ Codex sees via a managed projection gets no copy of its own, and an intact managed legacy copy of it is
406
+ removed through uninstall against that legacy dir (backup, transaction, rollback). Returns the names
407
+ still to install."""
289
408
  from . import projection
290
409
  for name, kind in sorted(projection.links(home, store).items()):
291
410
  if kind == "modified" and name in names:
292
411
  entry["kept"].append({"name": name, "status": "uniflow_link_modified",
293
- "reason": guide.BROKEN_REASON + "; a cópia nativa em ~/.agents/skills continua valendo",
412
+ "reason": guide.BROKEN_REASON + "; o link do Uniflow ocupa este nome no diretório do "
413
+ "Codex, então conserte a entrada do store",
294
414
  "fix": guide.broken_fix(name)})
295
415
  projected = projection.projected("codex", home, store)
296
416
  if not projected:
@@ -310,18 +430,20 @@ def codex_projection_step(args, home, store, entry, names, install, waiting, sto
310
430
  for name in dupes["modified"]:
311
431
  if name in selected and not overwrite:
312
432
  entry["kept"].append({"name": name, "status": "duplicate_modified",
313
- "reason": "cópia nativa editada que duplica a projeção do Uniflow no Codex",
433
+ "reason": "cópia editada na pasta compartilhada antiga, que duplica a projeção do Uniflow no Codex",
314
434
  "fix": "revise e, para remover com backup: crow-skills setup --agents codex --overwrite-modified --yes"})
315
435
  for name in dupes["shared"]:
316
436
  if name in selected:
317
437
  entry["kept"].append({"name": name, "status": "duplicate_shared",
318
- "reason": "cópia nativa também registrada para shared (diretório lido por outros hosts); "
319
- "mantida, e o Codex a vê duas vezes",
438
+ "reason": "cópia na pasta compartilhada antiga também registrada para outro agente que lê "
439
+ "aquele diretório; mantida, e o Codex a vê duas vezes",
320
440
  "fix": "se nenhum outro host precisa dela: crow-skills uninstall " + name +
321
- " --agents codex,shared --global --yes"})
441
+ " --dest " + str(legacy_shared_dir(home)) + " --yes"})
322
442
  if remove:
323
443
  cli = _cli()
324
- argv = ["uninstall", *remove, "--agents", "codex", "--global", "--json", "--home", str(home),
444
+ # The duplicates live in the legacy shared folder, never in Codex's own dir: --dest targets it
445
+ # explicitly, since no agent name points there any more.
446
+ argv = ["uninstall", *remove, "--dest", str(legacy_shared_dir(home)), "--json", "--home", str(home),
325
447
  "--dry-run" if dry_run else "--yes"]
326
448
  if overwrite:
327
449
  argv.append("--overwrite-modified")
@@ -473,6 +595,16 @@ def summarize(detected, entries, store, home, probe, dry_run, trust=True):
473
595
  parts.append(str(len(entry["removed"])) + (" a remover" if dry_run else " removidas") + " (duplicavam o Uniflow)")
474
596
  if kept:
475
597
  parts.append(str(len(kept)) + " mantidas")
598
+ legacy = entry.get("legacy_shared") or {}
599
+ if legacy.get("removed"):
600
+ # Apagar diretórios do usuário nunca é silencioso: aparece na linha do agente e na nota,
601
+ # com o caminho, para quem só lê a saída humana saber o que saiu.
602
+ parts.append(str(len(legacy["removed"])) + (" a tirar" if dry_run else " tiradas") + " da pasta compartilhada antiga")
603
+ item["notes"].append(("a remover de " if dry_run else "removidas de ") + legacy.get("path", "?") + ": " +
604
+ ", ".join(legacy["removed"]))
605
+ elif legacy.get("state") == "warn":
606
+ item["notes"].append("pasta compartilhada antiga " + legacy.get("path", "?") + ": " +
607
+ (legacy.get("note") or ("mantidas " + ", ".join(legacy.get("kept") or []))))
476
608
  item["skills"] = ", ".join(parts) or "—"
477
609
  guides = entry.get("guides") or {}
478
610
  guide_change = next((c for c in guides.get("changes", []) if c.get("agent") == cli["agent"]), None)
@@ -1,37 +1,31 @@
1
1
  """Uniflow's projection of its store into Codex's own skills dir ($CODEX_HOME/skills, default ~/.codex/skills).
2
2
 
3
- Codex scans both $CODEX_HOME/skills and ~/.agents/skills (where crow-skills installs for codex), follows
4
- symlinks and lists a same-name skill twice when the two entries resolve to different directories. Uniflow
5
- links ~/.codex/skills/crow-x -> <store>/crow-x, so a native copy in ~/.agents/skills duplicates it.
3
+ Since 0.15.0 crow-skills installs for codex in that same directory, like it does for every other CLI, so a
4
+ managed copy and a projected link can no longer carry the same name in two places. Codex still scans
5
+ ~/.agents/skills as well, so a copy left there by an older version is still listed twice: those legacy copies
6
+ are what duplicates() reports, and removing them is the migration away from the shared folder.
6
7
 
7
8
  A link counts as the codex install of a skill ("via uniflow") only when it resolves to <store>/<name> and
8
9
  that store entry is crow-skills-managed (guide.store_managed); a store entry Uniflow disabled is reported as
9
10
  disabled; a link to anything else is ignored. A link ~/.codex/skills/crow-x -> ~/.agents/skills/crow-x is an
10
- alias Codex deduplicates by canonical path: harmless, never touched.
11
+ alias Codex deduplicates by canonical path: never altered nor removed. Note that the LINK being untouched
12
+ does not make the command succeed — that name now lives in the codex destination, and a symlinked entry
13
+ there cannot be managed, so install/setup report it (status "unsafe") and exit 1 until the user resolves it.
11
14
  """
12
15
  import os
13
16
  from pathlib import Path
14
17
 
15
- from .agents import destinations, real_home, uniflow_store
18
+ from .agents import codex_home, codex_skills_dir, destinations, legacy_shared_dir, real_home, uniflow_store
16
19
  from .source import CrowError
17
20
 
18
21
 
19
- def codex_home(home):
20
- """$CODEX_HOME for the real profile (as Codex resolves it); an isolated --home uses <home>/.codex."""
21
- home = Path(home)
22
- if real_home(home) and os.environ.get("CODEX_HOME"):
23
- return Path(os.path.abspath(Path(os.environ["CODEX_HOME"]).expanduser()))
24
- return Path(os.path.abspath(home / ".codex"))
25
-
26
-
27
- def codex_skills_dir(home):
28
- return codex_home(home) / "skills"
29
-
30
-
31
22
  def links(home, store):
32
23
  """{name: "managed" | "disabled" | "modified" | "unmanaged" | "alias" | "other"} for crow-* symlinks in Codex's own
33
24
  skills dir: to an intact managed / Uniflow-disabled / edited-or-incomplete managed / hand-written store entry, to our
34
- own copy, or elsewhere. Only "managed" counts as the codex install and allows removing a native duplicate."""
25
+ legacy shared copy, or elsewhere. Only "managed" counts as the codex install and allows removing a legacy
26
+ duplicate. "alias" is a link into the legacy ~/.agents/skills: Codex deduplicates it by canonical path, and
27
+ crow-skills never alters it — but it occupies the name in the destination, so that name is reported
28
+ unmanageable rather than installed."""
35
29
  directory = codex_skills_dir(home)
36
30
  try:
37
31
  if directory.is_symlink() or not directory.is_dir():
@@ -42,14 +36,16 @@ def links(home, store):
42
36
  if not entries:
43
37
  return {}
44
38
  store_dir = uniflow_store(home)
45
- native = Path(destinations("codex", home)[0]["path"])
39
+ # The alias case is a link into the LEGACY shared folder, never into Codex's own dir (where the link
40
+ # itself lives): comparing against the destination would now test a link against itself.
41
+ legacy = legacy_shared_dir(home)
46
42
  from .guide import store_statuses
47
43
  statuses = store_statuses(home, store) # only an intact ("installed") store entry is a working codex install
48
44
  result = {}
49
45
  for link in entries:
50
46
  name = link.name
51
47
  resolved = os.path.realpath(link)
52
- if resolved == os.path.realpath(native / name):
48
+ if resolved == os.path.realpath(legacy / name):
53
49
  result[name] = "alias"
54
50
  elif (resolved == os.path.realpath(store_dir / name) and (store_dir / name).is_dir()
55
51
  and not (store_dir / name).is_symlink()):
@@ -62,7 +58,13 @@ def links(home, store):
62
58
 
63
59
 
64
60
  def status_rows(home, store, rows):
65
- """Rows for `status`/`doctor`: Codex's view through Uniflow's links, as a destination of its own."""
61
+ """Rows for `status`/`doctor`: Codex's view through Uniflow's links, classified.
62
+
63
+ Codex's own dir IS the codex destination since 0.15.0, so `store.status()` already lists these
64
+ links (as a bare `uniflow_link`). These rows ENRICH the existing one for each name — managed,
65
+ disabled, edited or unmanaged, each with its reason — and are only appended when the base scan
66
+ did not report the name. Appending unconditionally would count every projected skill twice.
67
+ """
66
68
  directory = str(codex_skills_dir(home))
67
69
  extra = []
68
70
  for name, kind in sorted(links(home, store).items()):
@@ -84,7 +86,22 @@ def status_rows(home, store, rows):
84
86
  "activation_verified": False, "projection": "uniflow_link_unmanaged",
85
87
  "note": "Link do Uniflow para um pacote do store que o crow-skills não gerencia; não conta como instalada. "
86
88
  "Revise o store e, para gerenciá-lo com backup: crow-skills setup --agents uniflow --replace --yes"})
87
- return extra
89
+ return merge_rows(rows, extra, directory)
90
+
91
+
92
+ def merge_rows(rows, extra, directory):
93
+ """Each enriched row replaces the base row of the same name in the same directory; the rest is
94
+ appended. Returns the merged list (the caller keeps its own list object)."""
95
+ merged, used = [], set()
96
+ for row in rows:
97
+ richer = next((e for e in extra if e["name"] == row.get("name") and row.get("target") == directory), None)
98
+ if richer is None:
99
+ merged.append(row)
100
+ continue
101
+ used.add(richer["name"])
102
+ merged.append(dict(row, **{k: v for k, v in richer.items() if k != "revision" or row.get("revision") is None}))
103
+ merged += [e for e in extra if e["name"] not in used]
104
+ return merged
88
105
 
89
106
 
90
107
  def projected(agent, home, store):
@@ -99,17 +116,42 @@ def native_target(agent, target, home):
99
116
  return agent == "codex" and os.path.abspath(target["path"]) == os.path.abspath(destinations("codex", home)[0]["path"])
100
117
 
101
118
 
119
+ def legacy_leftovers(home, store):
120
+ """Names crow-skills still has installed in the legacy shared folder, INDEPENDENT of Uniflow.
121
+
122
+ duplicates() answers "what duplicates a projection", so on a machine without Uniflow it is
123
+ always empty — and an install made by 0.14.0 (which put codex's copies in that folder) would be
124
+ invisible to status/doctor while Codex kept loading it. This is the signal for that."""
125
+ legacy = legacy_shared_dir(home)
126
+ if not legacy.is_dir() or legacy.is_symlink():
127
+ return []
128
+ target = {"path": str(legacy), "agents": ["codex"]}
129
+ try:
130
+ records = store.state(target["path"])["installed"]
131
+ if not records:
132
+ return []
133
+ rows = {r["name"]: r for r in store.status([target], sorted(records)) if r["name"]}
134
+ except (CrowError, OSError):
135
+ return []
136
+ return sorted(n for n, r in rows.items() if r["status"] in ("installed", "modified", "missing"))
137
+
138
+
102
139
  def duplicates(home, store):
103
- """Native copies in ~/.agents/skills that duplicate a managed projection, by what can be done with them.
140
+ """Copies left in the legacy shared folder (~/.agents/skills) that Codex lists a second time.
104
141
 
105
- Returns {"remove": [...], "modified": [...], "shared": [...]}: "remove" are crow-skills-managed and intact;
106
- "modified" were edited locally (kept unless --overwrite-modified); "shared" are also recorded for another
107
- agent of that dir (shared), which may rely on them."""
142
+ Codex scans its own dir and ~/.agents/skills; until 0.15.0 crow-skills installed for codex in the
143
+ latter. A copy still there duplicates what Uniflow projects into Codex's own dir, so it is the thing
144
+ to migrate away. Returns {"remove": [...], "modified": [...], "shared": [...]}: "remove" are
145
+ crow-skills-managed and intact; "modified" were edited locally (kept unless --overwrite-modified);
146
+ "shared" are also recorded for another agent of that dir, which may rely on them."""
108
147
  names = projected("codex", home, store)
109
148
  result = {"remove": [], "modified": [], "shared": []}
110
149
  if not names:
111
150
  return result
112
- target = destinations("codex", home)[0]
151
+ legacy = legacy_shared_dir(home)
152
+ if not legacy.is_dir() or legacy.is_symlink():
153
+ return result
154
+ target = {"path": str(legacy), "agents": ["codex"]}
113
155
  try:
114
156
  rows = {r["name"]: r for r in store.status([target], sorted(names)) if r["name"]}
115
157
  records = store.state(target["path"])["installed"]
@@ -785,9 +785,10 @@ def fixture_reading():
785
785
  return data
786
786
 
787
787
 
788
- def probe(home, processes=True, use_cache=False, platform=None):
788
+ def probe(home, processes=True, use_cache=False, platform=None, write_through=True):
789
789
  """The crow-resources/1 document. use_cache: reuse a reading younger than cacheSeconds
790
- (hooks, status lines, snapshot); processes: also scan agent CLI process trees."""
790
+ (hooks, status lines, snapshot); processes: also scan agent CLI process trees;
791
+ write_through: refresh the cache file on a miss (false for a side-effect-free probe)."""
791
792
  cfg, meta = load_config(home)
792
793
  reading, age, cached = None, 0, False
793
794
  data = fixture_reading()
@@ -799,7 +800,8 @@ def probe(home, processes=True, use_cache=False, platform=None):
799
800
  cached = reading is not None
800
801
  if reading is None:
801
802
  reading = read_machine(platform)
802
- write_cache(home, reading)
803
+ if write_through:
804
+ write_cache(home, reading)
803
805
  agents = read_processes(platform) if processes else None
804
806
  return build(reading, cfg, meta, agents, cached, age)
805
807
 
@@ -177,6 +177,8 @@ class Store:
177
177
  try:
178
178
  if data["id"] != txid or data["phase"] not in ("prepared", "restoring", "committed", "rolled_back"):
179
179
  raise ValueError("phase")
180
+ if "linked" in data and not re.fullmatch(r"[a-f0-9]{32}", data["linked"] or ""):
181
+ raise ValueError("linked")
180
182
  targets = data["before"]
181
183
  if not isinstance(targets, dict) or set(targets) != set(data["after"]):
182
184
  raise ValueError("targets")
@@ -344,7 +346,11 @@ class Store:
344
346
  }
345
347
  return {"before": before, "after": after, "items": items, "packages": packages, "changes": output}
346
348
 
347
- def apply(self, plan):
349
+ def apply(self, plan, linked=None):
350
+ """linked: a committed transaction this one completes, so a rollback of that one undoes this
351
+ one first. Used by the migration out of the legacy shared folder, which runs as its own
352
+ uninstall right after an install: without the link, rolling back the install alone would
353
+ remove the new copies while the legacy ones stayed removed, leaving the user with none."""
348
354
  if not plan["items"]:
349
355
  return None
350
356
  with self.lock():
@@ -359,6 +365,8 @@ class Store:
359
365
  txid = uuid.uuid4().hex
360
366
  data = {k: plan[k] for k in ("before", "after", "items")}
361
367
  data.update(id=txid, phase="prepared", created_at=datetime.now(timezone.utc).isoformat())
368
+ if linked:
369
+ data["linked"] = linked
362
370
  journal = self.root / "transactions" / (txid + ".json")
363
371
  try:
364
372
  for target in plan["before"]:
package/crow_skills/ui.py CHANGED
@@ -160,7 +160,7 @@ FIXES = {
160
160
  "unmanaged_conflict": "revise o conteúdo; para substituir com backup, acrescente --replace",
161
161
  "unmanaged_link": "acrescente --replace para trocar só o link por uma cópia gerenciada",
162
162
  "guide_requires_skill": "crow-skills setup --yes",
163
- "unknown_agent": "use --agents com: claude, codex, gemini, devin, shared, uniflow",
163
+ "unknown_agent": "use --agents com: claude, codex, gemini, devin, uniflow (um diretório fora desses: --dest)",
164
164
  "unknown_group": "crow-skills groups (lista os grupos; todas = catálogo inteiro)",
165
165
  "source_mismatch": "use install com --source para trocar de origem",
166
166
  "unsupported_platform": "agende crow-skills upgrade --yes por conta própria (ex.: cron)",
package/docs/cli.md CHANGED
@@ -18,7 +18,7 @@ crow-skills init --yes # alias
18
18
  | CLI | Binário | Configuração | Agente do instalador |
19
19
  |---|---|---|---|
20
20
  | Claude Code | `claude` | `~/.claude` | claude |
21
- | Codex CLI | `codex` | `~/.codex` | codex (`~/.agents/skills`) |
21
+ | Codex CLI | `codex` | `~/.codex` | codex (`$CODEX_HOME/skills`, padrão `~/.codex/skills`) |
22
22
  | Gemini CLI | `gemini` | `~/.gemini/settings.json` | gemini |
23
23
  | Antigravity | `agy` | `~/.gemini/antigravity-cli` | gemini (mesmo diretório de skills; hooks e status line próprios) |
24
24
  | Devin CLI | `devin` | `~/.config/devin` (ou XDG) | devin |
@@ -183,14 +183,13 @@ Erros retornam `ok:false` e `error.code/message`. Saídas reais incluem alteraç
183
183
 
184
184
  | Adaptador | Usuário | Projeto |
185
185
  |---|---|---|
186
- | codex | `~/.agents/skills` | `.agents/skills` |
186
+ | codex | `$CODEX_HOME/skills` (padrão `~/.codex/skills`) | `.codex/skills` |
187
187
  | claude | `~/.claude/skills` | `.claude/skills` |
188
188
  | gemini | `~/.gemini/skills` | `.gemini/skills` |
189
189
  | devin | `~/.config/devin/skills` (ou XDG_CONFIG_HOME) | `.devin/skills` |
190
- | shared | `~/.agents/skills` | `.agents/skills` |
191
190
  | uniflow | store do Uniflow: `~/.config/uniflow/skills` (veja [Uniflow](#uniflow)) | não existe; recusado com `unsupported_scope` |
192
191
 
193
- 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.
192
+ Cada adaptador grava no diretório PRÓPRIO do seu CLI: o crow-skills atua em cada CLI diretamente e não mantém pasta compartilhada de skills. Até a 0.14.0 o codex era a exceção, instalando em `~/.agents/skills`, o que fazia o crow-skills agir sobre um diretório em vez de sobre um agente: aparecia um agente `shared` que não é CLI nenhum, e o Codex, que varre as duas raízes, listava a mesma skill duas vezes. O adaptador `shared` foi removido e `~/.agents/skills` virou apenas legado: nada é instalado lá, e o que ficou de versões anteriores é apontado como duplicata para você remover (veja [Codex e a projeção do Uniflow](#codex-e-a-projeção-do-uniflow)). Para gravar num diretório fora dos adaptadores, use `--dest` explicitamente.
194
193
 
195
194
  `agents` só verifica PATH e caminhos, sem iniciar agente/LLM. `status`/`doctor` conferem arquivos e estado de instalação, não provam carregamento, ativação, independência dos workers ou execução correta da skill. Gemini oferece `/skills reload`; para outros hosts, confira sua lista/reload ou reinicie a sessão conforme a versão. Nenhum system prompt base é alterado; o guia abaixo e os hooks do modo crow, aplicados por padrão em `--global` e desligáveis com `--no-guide` e `--no-integrations`, são aditivos: só informam ao agente o modo escolhido pelo usuário.
196
195
 
@@ -214,18 +213,31 @@ O Uniflow guarda as skills num store próprio e cria, ao subir cada CLI, links d
214
213
  - Só `--global`: o Uniflow não tem store de projeto (`--project` falha com `unsupported_scope`).
215
214
  - 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.
216
215
  - Detecção: `agents` marca `detected` quando `uniflow` está no PATH (`executable_found`) ou o diretório de configuração existe (`config_found`).
217
- - 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.
216
+ - Cópias nativas duplicadas: se claude, gemini ou devin já têm um pacote crow de mesmo nome, o Uniflow não cria o link para ele e a cópia nativa continua valendo ali, possivelmente em outra revisão (`effect: skipped`). O codex se comporta igual desde a 0.15.0, por instalar no diretório dele (`effect: skipped`); antes ele instalava na pasta compartilhada e o Codex via a mesma skill duas vezes. `install`/`update` com `uniflow` listam essas cópias em `uniflow.native_duplicates` (diretórios que não puderam ser lidos, como um `~/.claude` symlink de dotfiles, vão para `uniflow.uninspected` sem derrubar o comando) e sugerem em `uniflow.suggested_command` o `uninstall` explícito; nada é removido sem esse comando. Remover a cópia nativa da crow-behaviour remove também o guia daquele agente, que deixa de valer fora do Uniflow.
218
217
  - 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`.
219
218
 
219
+ #### Exercitar um hook sem efeito colateral (`--probe`)
220
+
221
+ `crow_hook.py hook --host <host> --event <evento> --probe` calcula exatamente o que o hook entregaria naquele evento — modo crow, aviso de memória e o bloco do crow-broadcast — e **não grava nada**: nenhum evento no barramento, nenhum snapshot de sessão, nenhuma presença, nenhuma escrita de modo. É o que usar para conferir se o hook de um CLI está cabeado e funcionando.
222
+
223
+ Sem isso, validar um hook rodando-o de verdade registra uma sessão com o `session_id` usado no teste. Essa sessão falsa conta como viva para TODOS os outros agentes da máquina pela janela de `stale_minutes` (30 min por padrão): entra na contagem do bloco, na relevância por repositório e nos avisos de "não troque de branch neste clone". Uma validação dos cinco CLIs chega a inventar dez ou mais sessões `working`.
224
+
220
225
  #### Codex e a projeção do Uniflow
221
226
 
222
- O Codex lê duas raízes de skills do usuário: `$CODEX_HOME/skills` (padrão `~/.codex/skills`, onde o Uniflow liga o store) e `~/.agents/skills` (onde o crow-skills instala para o codex). Ele segue symlinks e lista duas vezes uma skill de mesmo nome quando as duas entradas apontam para diretórios diferentes. Por isso:
227
+ O Codex lê duas raízes de skills do usuário: `$CODEX_HOME/skills` (padrão `~/.codex/skills`, onde o crow-skills instala e o Uniflow liga o store) e a legada `~/.agents/skills`, onde o crow-skills instalava para o codex até a 0.14.0. Ele segue symlinks e lista duas vezes uma skill de mesmo nome quando as duas entradas apontam para diretórios diferentes — hoje isso só acontece com uma cópia deixada na pasta legada. Instalar no diretório do próprio Codex é seguro ao lado do Uniflow: ele não sobrescreve um nome que já existe ali (registra `skipped`) e só apaga um diretório que tenha o marcador `.uniflow-store.json`, que apenas ele escreve. Por isso:
223
228
 
224
229
  - Um link `~/.codex/skills/crow-x` → `<store>/crow-x`, com a entrada do store gerenciada pelo crow-skills, conta como a instalação do codex (`via uniflow`). Entrada desabilitada no Uniflow aparece como `disabled`; entrada escrita à mão, como não gerenciada (com o mesmo motivo e correção do resto do Uniflow). `CODEX_HOME` vale para o perfil real; `--home` isolado usa `<home>/.codex`.
225
- - `setup` e `upgrade` não criam cópia nativa em `~/.agents/skills` para esses nomes e removem, pelo `uninstall` normal (backup, transação, `rollback`), as cópias nativas gerenciadas e intactas que duplicam a projeção (`removida: duplicava a projeção do Uniflow`). Uma cópia editada fica, com aviso e a dica `--overwrite-modified`. Uma cópia também registrada para `shared` (`--agents codex,shared`: o diretório é lido por outros hosts) fica, porque outro host pode depender dela; o motivo e o comando aparecem no resumo.
226
- - `install --agents codex` não cria cópia nativa de um nome projetado (`codex_projection.via_uniflow` no resumo) e nunca remove nada: as duplicatas existentes aparecem em `codex_projection.duplicates`, com `suggested_command`. Com `--agents codex,shared` instala como antes.
230
+ - `setup` e `upgrade` não criam cópia própria para esses nomes e removem, pelo `uninstall` normal contra a pasta legada (`--dest ~/.agents/skills`, com backup, transação e `rollback`), as cópias gerenciadas e intactas que ficaram lá duplicando a projeção (`removida: duplicava a projeção do Uniflow`). Uma cópia editada fica, com aviso e a dica `--overwrite-modified`. Uma cópia também registrada para outro agente daquele diretório fica, porque outro host pode depender dela; o motivo e o comando (`uninstall ... --dest ~/.agents/skills`) aparecem no resumo.
231
+ - `install --agents codex` não cria cópia de um nome projetado (`codex_projection.via_uniflow` no resumo) e nunca remove nada: as cópias legadas aparecem em `codex_projection.duplicates`, com `suggested_command`. `--agents shared` não existe mais e é recusado com `unknown_agent`; para gravar num diretório específico, use `--dest`.
227
232
  - A crow-behaviour continua nativa onde o Uniflow não a projeta, e o guia, as integrações e os hooks do Codex seguem funcionando; quando ela é projetada, o guia conta a projeção.
228
- - Um link `~/.codex/skills/crow-x` → `~/.agents/skills/crow-x` é só um apelido (o Codex o deduplica pelo caminho canônico): nunca é alterado nem removido.
233
+ - Migração da pasta compartilhada antiga: em `setup`, `upgrade` e no autoupdate, depois de instalar no diretório do Codex, o crow-skills remove de `~/.agents/skills` as skills que ELE instalou lá e que estão íntegras, pelo `uninstall` normal (`--dest ~/.agents/skills`, com backup, transação e `rollback`); o resumo do agente traz isso em `legacy_shared`. Isso cobre também uma skill que o Uniflow não projeta (a própria crow-behaviour, tipicamente), que não apareceria como duplicata e ficaria lá para sempre. Uma cópia editada, ou também registrada para outro agente daquele diretório, é mantida e reportada em `kept` com o comando. Nada que não tenha sido instalado pelo crow-skills é tocado, e uma falha na migração nunca derruba o resultado do agente (vira `legacy_shared.state: warn`).
234
+ - A cópia legada só sai quando aquele nome REALMENTE funciona no diretório do Codex — instalação íntegra nossa, ou link saudável do Uniflow. Se a entrada do store do Uniflow está quebrada, o link ocupa o nome sem funcionar e a cópia legada é a única skill que o Codex tem: nesse caso ela é mantida e reportada (`legacy_shared_without_replacement`), porque removê-la tiraria a skill em vez de migrá-la. O mesmo vale quando o nome no diretório do Codex é um apelido apontando para a própria cópia legada: remover o alvo deixaria um link pendurado. Cada nome recebe UMA razão em `kept`: uma razão mais específica já registrada para ele (projeção quebrada, destino inseguro) prevalece.
235
+ - **Mudanças de comportamento do destino do codex (0.15.0).** Com o destino no diretório próprio do Codex, duas situações que antes não afetavam o codex passam a afetá-lo, exatamente como já valia para claude, gemini e devin: (a) se `~/.codex` (ou o `$CODEX_HOME`) for um symlink, o comando é recusado com `unsafe_destination` — o crow-skills não escreve através de symlink, e a saída pede um caminho físico ou `--dest`; (b) se houver um link de qualquer tipo ocupando o nome no diretório (inclusive o apelido para a pasta legada), aquele nome não pode ser gerenciado: ele é reportado com status `unsafe` e o comando sai com 1, sem alterar o link. Não é uma regra nova do codex, é a mesma regra dos outros CLIs passando a valer para ele.
236
+ - **`$CODEX_HOME` relativo é ignorado.** Um valor relativo seria resolvido contra o diretório de trabalho DESTE processo, que não é o do Codex, então o destino dependeria de onde o comando foi rodado e poderia cair dentro de um repositório seu. Só valor absoluto (ou com `~`) é honrado; caso contrário vale `<home>/.codex`.
237
+ - **Fallback perdido quando a entrada do store do Uniflow está quebrada.** Até a 0.14.0 o codex tinha duas raízes, então uma entrada quebrada no store ainda deixava uma cópia funcional na pasta compartilhada. Agora o link do Uniflow ocupa o nome no diretório do Codex: a entrada quebrada é reportada (`uniflow_link_modified`, com o comando que conserta o store) e a skill fica sem versão funcional até você consertar. A cópia legada, se existir, NÃO é removida nesse caso — é justamente o que o guarda de substituição protege.
238
+ - **Aviso `codex_legacy_shared` em `status`/`doctor`.** Numa máquina sem Uniflow nada do que ficou na pasta compartilhada duplica uma projeção, então o aviso de duplicatas não apareceria e uma instalação feita pela 0.14.0 ficaria invisível, ainda carregada pelo Codex. Esse aviso novo lista essas skills, com o caminho e o comando (`setup --agents codex --yes`, que migra, ou `uninstall … --dest <pasta legada>`).
239
+ - **Rollback encadeado.** A migração roda como um `uninstall` próprio logo depois do install e fica atrelada a ele (campo `linked` no journal da transação). Um `rollback` da transação do install desfaz PRIMEIRO a migração, restaurando as cópias legadas, e só então remove a instalação nova: o estado final é o anterior ao comando. Sem esse encadeamento, desfazer só o install deixaria as duas pastas vazias.
240
+ - Um link `~/.codex/skills/crow-x` → `~/.agents/skills/crow-x` (apontando para a pasta legada) é só um apelido (o Codex o deduplica pelo caminho canônico): nunca é alterado nem removido.
229
241
  - `status`/`doctor` mostram `~/.codex/skills` como um destino do codex (`N skills (N via uniflow)`). Duplicatas geram o aviso `codex_duplicates` (`warnings`) com o comando que resolve; o doctor continua `healthy:true`, porque as duas cópias estão íntegras e funcionam (o Codex só as lista duas vezes).
230
242
 
231
243
 
@@ -246,7 +258,7 @@ crow-skills guide uninstall --agents codex --global --yes
246
258
 
247
259
  Aplicação automática (padrão em `install`, `update` e `uninstall` com `--global`, desligada por `--no-guide`):
248
260
 
249
- - 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.
261
+ - O guia é planejado junto com as skills, a partir do conjunto de skills que o agente terá depois do comando, e conferido antes de gravar qualquer coisa. Install/update que deixam a crow-behaviour instalada aplicam o guia ou o regeneram; mudança de outra skill gerenciada regenera um guia já instalado (também quando a mudança chega pelo diretório do codex). Uninstall da crow-behaviour remove o guia, devolvendo o arquivo que `--replace` havia deslocado; uninstall de outra skill regenera o guia instalado e nunca cria um novo.
250
262
  - 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.
251
263
  - `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.
252
264
  - `--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`.
@@ -527,6 +539,6 @@ Cada execução:
527
539
 
528
540
  1. Atualiza o próprio crow-skills conforme a instalação: Homebrew (`brew upgrade andersonflima/tap/crow-skills`), npm global (`npm install -g @andespindola/crow-skills@latest`). Checkout Git só com `autoupdate on --git-pull`, e só se estiver limpo em `main`/`master` (`git pull --ff-only`); caso contrário, é pulado com o motivo. Saída de brew/npm/git não é guardada nem impressa, só código de saída e versões.
529
541
  2. Se a versão mudou, a versão nova (o launcher) faz o passo seguinte, para usar o snapshot novo como fonte.
530
- 3. Para cada agente com skills gerenciadas (claude, codex/`~/.agents/skills`, gemini, devin, uniflow), roda o equivalente a `update <skills> --agents <agente> --global --yes`: regenera guia, integrações e hooks; a [identidade do pacote](#origem-e-versões) evita `source_mismatch` depois do upgrade. Skills editadas localmente (`modified`), ausentes ou instaladas de outra origem (`other_origin`, por exemplo de um clone Git) ficam como estão e aparecem em `kept`; nada é sobrescrito. Uma reaplicação dos hooks troca as entradas do crow-skills no lugar, sem mudar a posição das suas. Uma recusa num agente (por exemplo, guia editado) aparece em `error` e não interrompe os outros.
542
+ 3. Para cada agente com skills gerenciadas (claude, codex/`$CODEX_HOME/skills`, gemini, devin, uniflow), roda o equivalente a `update <skills> --agents <agente> --global --yes`: regenera guia, integrações e hooks; a [identidade do pacote](#origem-e-versões) evita `source_mismatch` depois do upgrade. Skills editadas localmente (`modified`), ausentes ou instaladas de outra origem (`other_origin`, por exemplo de um clone Git) ficam como estão e aparecem em `kept`; nada é sobrescrito. Uma reaplicação dos hooks troca as entradas do crow-skills no lugar, sem mudar a posição das suas. Uma recusa num agente (por exemplo, guia editado) aparece em `error` e não interrompe os outros.
531
543
 
532
544
  O resultado fica em `~/.local/state/crow-skills/autoupdate/state.json` (`lastRun` e as últimas 20 execuções), aparece em `autoupdate status`, em `status`/`doctor` (doctor `healthy:false` se a última execução falhou ou o agendador foi alterado/removido) e em `events snapshot`. Duas execuções simultâneas são recusadas (`busy`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/crow-skills",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Versioned, complementary agent skills with a protected multi-agent installer",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/release.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "crow_skills/__init__.py": {
19
19
  "executable": false,
20
20
  "kind": "file",
21
- "sha256": "f246c05e18f6f675c3819d7a9f59e592f8b5da30e46c811f5d9a145aa004eea2"
21
+ "sha256": "26eaa6dfbdfbc6e83983270615e53556227560f236d3064f8fb596bbfceebf0c"
22
22
  },
23
23
  "crow_skills/__main__.py": {
24
24
  "executable": false,
@@ -28,12 +28,12 @@
28
28
  "crow_skills/agents.py": {
29
29
  "executable": false,
30
30
  "kind": "file",
31
- "sha256": "b14c37d2f1becfe38c74411fc413608b375c05bd6961a0757e3ec085ec2e1f66"
31
+ "sha256": "0f8a284d5ce6dfc8c55a81be861c56c1884156937ecbce3a8800aef4ade76496"
32
32
  },
33
33
  "crow_skills/autoupdate.py": {
34
34
  "executable": false,
35
35
  "kind": "file",
36
- "sha256": "bbe3c38f93d8f6e70c0ec428e3d383d7248c61c07c774420c4c47e27c7e9def5"
36
+ "sha256": "8de4a6f3f7ce773dafb4c4f9b217d399becbd98b0b0638a80e27e13450e3a0f6"
37
37
  },
38
38
  "crow_skills/broadcast.py": {
39
39
  "executable": false,
@@ -43,7 +43,7 @@
43
43
  "crow_skills/cli.py": {
44
44
  "executable": false,
45
45
  "kind": "file",
46
- "sha256": "f38bfcede958343266e1a6f3ac061e4321c04a4e264e5b96a91d93bb357b28a4"
46
+ "sha256": "5a1146f2d6c14554384bc13f0e2f83b3e5ce4b2743dd8a3fee70db3c2f5db9b1"
47
47
  },
48
48
  "crow_skills/events.py": {
49
49
  "executable": false,
@@ -58,12 +58,12 @@
58
58
  "crow_skills/guide.py": {
59
59
  "executable": false,
60
60
  "kind": "file",
61
- "sha256": "0d6fd0a026954f8d9b72bafcad204f1929f3b96f89f84e0bea1c0c5deda91cdc"
61
+ "sha256": "aa8c16b293a72d6aabd99246a3666e40beaa81d5c1b77e6389341eca463e6a7f"
62
62
  },
63
63
  "crow_skills/hook.py": {
64
64
  "executable": false,
65
65
  "kind": "file",
66
- "sha256": "526f7a7acf7c9e433d7c986c94aaa39d1603813107387601f27859a473ce6f52"
66
+ "sha256": "3a2b3df94cbd655f88f087a60640c016a5669c06657e97236c841ae19c3a3257"
67
67
  },
68
68
  "crow_skills/integrations.py": {
69
69
  "executable": false,
@@ -73,7 +73,7 @@
73
73
  "crow_skills/machine.py": {
74
74
  "executable": false,
75
75
  "kind": "file",
76
- "sha256": "f48bb54b7816ce0fc62e68694dd970f699d355e60db2fb2eab651291294c7012"
76
+ "sha256": "8b7187afae9ad85369fdc8a918e19b32bab9d95776246cda6670a963dff19df3"
77
77
  },
78
78
  "crow_skills/npm_entry.py": {
79
79
  "executable": false,
@@ -88,12 +88,12 @@
88
88
  "crow_skills/projection.py": {
89
89
  "executable": false,
90
90
  "kind": "file",
91
- "sha256": "0f6a7c788e58cc54752bbf293f107a2a8e731cacefa2dcd95d3f4998f2866d4a"
91
+ "sha256": "927cabbdde134c5c504abf6e762026295e6a7a38e8bee1f45a608d21b716be4a"
92
92
  },
93
93
  "crow_skills/resources.py": {
94
94
  "executable": false,
95
95
  "kind": "file",
96
- "sha256": "871e3834c342ee52e98a0999c679a8f329d7bc6d71d3008b21d08c806d39a7c3"
96
+ "sha256": "8cb3fe3a24ebd1d84d2c8b135f4c10b76928b3b94d5aa921c847855edb6213a6"
97
97
  },
98
98
  "crow_skills/source.py": {
99
99
  "executable": false,
@@ -103,7 +103,7 @@
103
103
  "crow_skills/storage.py": {
104
104
  "executable": false,
105
105
  "kind": "file",
106
- "sha256": "e3a7a41bcb3aa52530bbc1770165308b61a42017a8d8962fdb6a271ce2b5188f"
106
+ "sha256": "92b7b76ed75e84901dc2a67df3bb6998558200041bc42c8597e708829541a5b1"
107
107
  },
108
108
  "crow_skills/trust.py": {
109
109
  "executable": false,
@@ -113,7 +113,7 @@
113
113
  "crow_skills/ui.py": {
114
114
  "executable": false,
115
115
  "kind": "file",
116
- "sha256": "72f0ce4f47d5d3225ee30fe116b2b359bef3c426f5366d51782fa66eeedcc094"
116
+ "sha256": "24b5cb9aea11603db79c6c10628952263c265251f737cc3621db6872f8c15a91"
117
117
  },
118
118
  "docs/cli-security.md": {
119
119
  "executable": false,
@@ -123,12 +123,12 @@
123
123
  "docs/cli.md": {
124
124
  "executable": false,
125
125
  "kind": "file",
126
- "sha256": "88d12559d02a8266f832e25ff66f9494c7c68822d5a5a1101f65d289089a4d3c"
126
+ "sha256": "9dd1b9cc567e01356ead3d667b3ea559534d44930dffaa9dff7808e3476f25a3"
127
127
  },
128
128
  "package.json": {
129
129
  "executable": false,
130
130
  "kind": "file",
131
- "sha256": "62aad546f7a29256d2769ffb7741dd3911c048efa9147d70305b14aea0ad32a5"
131
+ "sha256": "f89633c7d12d50ab189edd74ed7c347d299001db6b9a0397f7a98693abaef936"
132
132
  },
133
133
  "skills/crow-accessibility/SKILL.md": {
134
134
  "executable": false,
@@ -716,7 +716,7 @@
716
716
  "sha256": "e37132ef8b060548191fbf14a5a18a9dd3acd84ea9a35e802174455b9e374d3d"
717
717
  }
718
718
  },
719
- "revision": "7f30848161287f341f72a4302f4ea685a2dab610",
719
+ "revision": "4297812328b55870f4b4e7a9bb3e4e7d4f82e358",
720
720
  "schema_version": 1,
721
- "version": "0.14.0"
721
+ "version": "0.15.0"
722
722
  }