@andespindola/crow-skills 0.14.0 → 0.16.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,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
 
@@ -19,6 +19,18 @@ from .agents import uniflow_store
19
19
  from .source import CrowError, MAX_FILE, MAX_PACKAGE, MAX_FILES, SHA, digest, file_entry, safe_relative, same_source, source_identity, valid_name
20
20
 
21
21
 
22
+ def resolved(path):
23
+ """checked() sobre o caminho FÍSICO: usado onde o alvo vem GRAVADO numa transação.
24
+
25
+ Um alvo registrado antes de o diretório virar dotfiles continua apontando para o caminho antigo.
26
+ Exigir ali um caminho livre de symlink quebrava rollback e recover e chegava a TRAVAR o perfil
27
+ (recover recusava, rollback dizia fase incompatível, e todo install passava a exigir recovery).
28
+ Resolver primeiro mantém a garantia: o que é escrito continua passando por checked().
29
+ """
30
+ from .agents import physical
31
+ return checked(physical(path))
32
+
33
+
22
34
  def checked(path):
23
35
  path = Path(os.path.abspath(Path(path).expanduser()))
24
36
  for part in reversed((path, *path.parents)):
@@ -132,6 +144,22 @@ def validate_entries(entries):
132
144
  raise CrowError("invalid_state", "Hash/tipo de arquivo inválido no estado.")
133
145
 
134
146
 
147
+ def retargeted(state, target):
148
+ """O manifesto gravado precisa declarar o caminho de HOJE (resolvido), não o de quando a
149
+ transação rodou: `validate_state` exige essa igualdade, e sem isto um rollback restaurava um
150
+ estado que o próprio leitor recusava depois com invalid_state."""
151
+ if not isinstance(state, dict):
152
+ return state
153
+ return dict(state, target=str(resolved(target)))
154
+
155
+
156
+ def same_manifest(a, b):
157
+ """Dois manifestos descrevem o mesmo conteúdo, ignorando a string do caminho do alvo."""
158
+ if a is None or b is None:
159
+ return a == b
160
+ return {k: v for k, v in a.items() if k != "target"} == {k: v for k, v in b.items() if k != "target"}
161
+
162
+
135
163
  def validate_state(data, target):
136
164
  try:
137
165
  if (data["schema_version"] != 1 or data["target"] != str(target) or
@@ -153,12 +181,50 @@ class Store:
153
181
  self.root = self.home / ".local/state/crow-skills"
154
182
 
155
183
  def state_path(self, target):
156
- key = hashlib.sha256(str(checked(target)).encode()).hexdigest()
184
+ key = hashlib.sha256(str(resolved(target)).encode()).hexdigest()
157
185
  return self.root / "targets" / key / "state.json"
158
186
 
187
+ def aliased_state_path(self, target):
188
+ """Estado gravado sob um APELIDO do mesmo destino (antes da resolução de symlink, 0.16.0).
189
+
190
+ A chave é o sha do caminho; um estado escrito quando ~/.claude ainda era diretório comum
191
+ está sob outra chave e ficaria invisível depois que o usuário move tudo para dotfiles —
192
+ `uninstall` responderia not_managed e a cópia instalada viraria órfã. Varre os destinos
193
+ conhecidos (poucos) e devolve o que resolve para o MESMO caminho físico."""
194
+ from .agents import physical
195
+ directory = self.root / "targets"
196
+ try:
197
+ if directory.is_symlink() or not directory.is_dir():
198
+ return None
199
+ entries = list(os.scandir(directory))
200
+ except OSError:
201
+ return None
202
+ wanted = physical(target)
203
+ found = []
204
+ for entry in entries:
205
+ candidate = Path(entry.path) / "state.json"
206
+ try:
207
+ data = read_json(candidate)
208
+ except (CrowError, OSError, ValueError):
209
+ continue
210
+ recorded = data.get("target") if isinstance(data, dict) else None
211
+ if isinstance(recorded, str) and recorded != str(target) and physical(recorded) == wanted:
212
+ found.append(candidate)
213
+ # Mais de um apelido para o MESMO caminho físico é ambíguo: escolher o primeiro do scandir
214
+ # (ordem arbitrária de hash) podia reportar uma skill gerenciada como `unmanaged` e, com
215
+ # --replace, sobrescrevê-la como se fosse lixo alheio — exatamente o dano que o apelido
216
+ # existe para evitar. Sem resposta única, nenhuma resposta.
217
+ return found[0] if len(found) == 1 else None
218
+
159
219
  def state(self, target):
160
- target = checked(target)
220
+ target = resolved(target)
161
221
  path = checked(self.state_path(target))
222
+ if not path.exists():
223
+ alias = self.aliased_state_path(target)
224
+ if alias is not None:
225
+ data = read_json(alias)
226
+ # Reescreve o alvo para o caminho físico; a próxima mutação grava sob a chave nova.
227
+ return validate_state(dict(data, target=str(target)), target)
162
228
  if not path.exists():
163
229
  return {"schema_version": 1, "target": str(target), "installed": {}}
164
230
  return validate_state(read_json(path), target)
@@ -177,11 +243,17 @@ class Store:
177
243
  try:
178
244
  if data["id"] != txid or data["phase"] not in ("prepared", "restoring", "committed", "rolled_back"):
179
245
  raise ValueError("phase")
246
+ if "linked" in data and not re.fullmatch(r"[a-f0-9]{32}", data["linked"] or ""):
247
+ raise ValueError("linked")
180
248
  targets = data["before"]
181
249
  if not isinstance(targets, dict) or set(targets) != set(data["after"]):
182
250
  raise ValueError("targets")
183
251
  for target, before in targets.items():
184
- if str(checked(target)) != target:
252
+ # Absoluto e normalizado, mas NÃO exigimos que continue livre de symlink: o destino
253
+ # pode ter virado dotfiles depois desta transação, e toda ESCRITA ainda passa por
254
+ # checked(). Exigir aqui derrubava status, doctor, install, uninstall e recover de
255
+ # TODOS os agentes por causa de um journal antigo.
256
+ if target != os.path.abspath(target):
185
257
  raise ValueError("path")
186
258
  validate_state(before, target)
187
259
  validate_state(data["after"][target], target)
@@ -221,7 +293,7 @@ class Store:
221
293
 
222
294
  def work(self, target, txid):
223
295
  from .guide import shielded
224
- target = checked(target)
296
+ target = resolved(target)
225
297
  key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
226
298
  # Beside the discovery dir, unless that parent is protected (the Uniflow
227
299
  # config): then staging/backups stay in the private state root instead.
@@ -232,7 +304,7 @@ class Store:
232
304
  """Backups of a transaction, wherever the version that made it put them: older
233
305
  transactions kept them beside the target even where that is now protected."""
234
306
  primary = self.work(target, txid)
235
- target = checked(target)
307
+ target = resolved(target)
236
308
  key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
237
309
  for candidate in (primary, target.parent / ".crow-skills" / key / txid, self.root / "work" / key / txid):
238
310
  if os.path.lexists(candidate):
@@ -344,7 +416,11 @@ class Store:
344
416
  }
345
417
  return {"before": before, "after": after, "items": items, "packages": packages, "changes": output}
346
418
 
347
- def apply(self, plan):
419
+ def apply(self, plan, linked=None):
420
+ """linked: a committed transaction this one completes, so a rollback of that one undoes this
421
+ one first. Used by the migration out of the legacy shared folder, which runs as its own
422
+ uninstall right after an install: without the link, rolling back the install alone would
423
+ remove the new copies while the legacy ones stayed removed, leaving the user with none."""
348
424
  if not plan["items"]:
349
425
  return None
350
426
  with self.lock():
@@ -359,6 +435,8 @@ class Store:
359
435
  txid = uuid.uuid4().hex
360
436
  data = {k: plan[k] for k in ("before", "after", "items")}
361
437
  data.update(id=txid, phase="prepared", created_at=datetime.now(timezone.utc).isoformat())
438
+ if linked:
439
+ data["linked"] = linked
362
440
  journal = self.root / "transactions" / (txid + ".json")
363
441
  try:
364
442
  for target in plan["before"]:
@@ -389,7 +467,7 @@ class Store:
389
467
  if item["new"] is not None:
390
468
  os.replace(work / "new" / item["name"], dest)
391
469
  for target, state in data["after"].items():
392
- atomic_json(self.state_path(target), state)
470
+ atomic_json(self.state_path(target), retargeted(state, target))
393
471
  data["phase"] = "committed"
394
472
  atomic_json(journal, data)
395
473
  except (Exception, KeyboardInterrupt):
@@ -415,10 +493,14 @@ class Store:
415
493
  operations = []
416
494
  for target, before in data["before"].items():
417
495
  current = self.state(target)
418
- if current != before and current != data["after"][target]:
496
+ # O campo "target" dentro do manifesto guarda o caminho de QUANDO a transação rodou; se
497
+ # o diretório virou dotfiles depois, ele difere do caminho resolvido de hoje e as duas
498
+ # estruturas nunca batem, recusando um rollback perfeitamente válido. A comparação é
499
+ # sobre o CONTEÚDO (as skills instaladas), não sobre a string do caminho.
500
+ if not same_manifest(current, before) and not same_manifest(current, data["after"][target]):
419
501
  raise CrowError("recovery_conflict", "Manifesto mudou após a transação; recuperação recusada.")
420
502
  for item in reversed(data["items"]):
421
- dest = checked(Path(item["target"])) / item["name"]
503
+ dest = resolved(Path(item["target"])) / item["name"]
422
504
  old = self.existing_work(item["target"], data["id"]) / "old" / item["name"]
423
505
  if item.get("link") is not None:
424
506
  if same_link(old, item["link"]):
@@ -454,7 +536,7 @@ class Store:
454
536
  if backup is not None:
455
537
  os.replace(backup, dest)
456
538
  for target, state in data["before"].items():
457
- atomic_json(self.state_path(target), state)
539
+ atomic_json(self.state_path(target), retargeted(state, target))
458
540
  data["phase"] = "rolled_back"
459
541
  atomic_json(self.root / "transactions" / (data["id"] + ".json"), data)
460
542
 
@@ -471,7 +553,10 @@ class Store:
471
553
  return rollback(self, data, self.home, recovery)
472
554
  if not recovery:
473
555
  for target, state in data["after"].items():
474
- if self.state(target) != state:
556
+ # same_manifest, e não ==: o campo "target" do manifesto guarda o caminho de
557
+ # quando a transação rodou, e um diretório que virou dotfiles depois faria toda
558
+ # comparação falhar, recusando um rollback válido por diferença de string.
559
+ if not same_manifest(self.state(target), state):
475
560
  raise CrowError("rollback_conflict", "Há mudanças posteriores; rollback antigo recusado.")
476
561
  self._restore(data)
477
562
 
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,25 @@ 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.
193
+
194
+ #### Diretório de skills em symlink (dotfiles)
195
+
196
+ Desde a 0.16.0 o destino de cada adaptador é resolvido para o caminho FÍSICO (`agents.physical`, via `os.path.realpath`): `~/.codex -> /dotfiles/codex` instala em `/dotfiles/codex/skills`, e o symlink é preservado. Vale para os cinco CLIs e também para `--dest`. Antes isso era recusado com `unsafe_destination` em todos eles.
197
+
198
+ O controle de segurança não foi enfraquecido, foi deslocado: `storage.checked()` continua recusando escrever ATRAVÉS de um symlink, e agora valida o destino real, o que é mais preciso. As proteções por LOCALIZAÇÃO continuam valendo depois da resolução: um diretório de skills que resolve para dentro de `~/.config/uniflow` é recusado com `protected_path`, sem tocar a config.
199
+
200
+ Duas consequências que vale conhecer:
201
+
202
+ - **O arquivo de prompt do agente não é resolvido.** `CLAUDE.md`, `AGENTS.md` e `rules/crow-skills.md` seguem recusados quando o caminho passa por symlink, porque gravar ali reescreveria um arquivo dentro do seu repositório de dotfiles. Isso não derruba mais o comando: o agente aparece em `guides.skipped` com `code: "unsafe_destination"` e as skills são instaladas. Para ter o guia nesse caso, grave-o você mesmo no caminho físico ou use `--no-guide` para não voltar a tentar.
203
+ - **Apontar o diretório de um CLI exatamente para o store do Uniflow é RECUSADO numa mutação** (`protected_path`, com a dica de usar `--agents uniflow`). Gravar no store vale para todos os CLIs que o Uniflow projeta, não só para o selecionado, e a resolução tornou esse caminho alcançável a partir da seleção de um CLI. A checagem roda só na mutação: `destinations()` é usado por `status`, `doctor` e pelos avisos do uniflow para inspecionar o destino de outro agente, e recusar ali derrubaria leitura.
204
+ - **As integrações (status line, hooks, comando) também passam por `protect()`**, então num diretório de agente em symlink elas são recusadas junto com o guia e aparecem em `integrations.skipped`. Um diretório `.crow-skills` de transações/backup aparece dentro do caminho físico (o repositório de dotfiles) e não é podado depois do commit.
194
205
 
195
206
  `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
207
 
@@ -214,18 +225,31 @@ O Uniflow guarda as skills num store próprio e cria, ao subir cada CLI, links d
214
225
  - Só `--global`: o Uniflow não tem store de projeto (`--project` falha com `unsupported_scope`).
215
226
  - 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
227
  - 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.
228
+ - 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
229
  - 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
230
 
231
+ #### Exercitar um hook sem efeito colateral (`--probe`)
232
+
233
+ `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.
234
+
235
+ 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`.
236
+
220
237
  #### Codex e a projeção do Uniflow
221
238
 
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:
239
+ 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
240
 
224
241
  - 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.
242
+ - `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.
243
+ - `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
244
  - 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.
245
+ - 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`).
246
+ - 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.
247
+ - **Mudanças de comportamento do destino do codex (0.15.0).** Com o destino no diretório próprio do Codex, uma situação que antes não afetava o codex passa a afetá-lo, exatamente como já valia para claude, gemini e devin: (a) a partir da 0.16.0 um `~/.codex`/`$CODEX_HOME` em symlink é RESOLVIDO para o caminho físico (veja a seção de dotfiles acima); na 0.15.0 ele era recusado com `unsafe_destination`; (b) se houver um link de qualquer tipo ocupando o nome no diretório (inclusive o apelido para a pasta legada), aquele nome não pode ser gerenciado: ele é reportado com status `unsafe` e o comando sai com 1, sem alterar o link. Não é uma regra nova do codex, é a mesma regra dos outros CLIs passando a valer para ele.
248
+ - **`$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`.
249
+ - **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.
250
+ - **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>`).
251
+ - **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.
252
+ - 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
253
  - `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
254
 
231
255
 
@@ -246,7 +270,7 @@ crow-skills guide uninstall --agents codex --global --yes
246
270
 
247
271
  Aplicação automática (padrão em `install`, `update` e `uninstall` com `--global`, desligada por `--no-guide`):
248
272
 
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.
273
+ - 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
274
  - 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
275
  - `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
276
  - `--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 +551,6 @@ Cada execução:
527
551
 
528
552
  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
553
  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.
554
+ 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
555
 
532
556
  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.16.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": "6a988b314e5b6bb3f2db799d74aec9717c6c3dfa59b4b1ebdeb1e3697fad498b"
22
22
  },
23
23
  "crow_skills/__main__.py": {
24
24
  "executable": false,
@@ -28,22 +28,22 @@
28
28
  "crow_skills/agents.py": {
29
29
  "executable": false,
30
30
  "kind": "file",
31
- "sha256": "b14c37d2f1becfe38c74411fc413608b375c05bd6961a0757e3ec085ec2e1f66"
31
+ "sha256": "1c2f61e6729c88f0ddc218fba0c5159869e5226d4f7bc0fd2181dd688d4272fe"
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,
40
40
  "kind": "file",
41
- "sha256": "33ba77b7fad64b8cb068d7cdac697505fbe061d60522ef0fabb9b121255cee2d"
41
+ "sha256": "3d25bd5fa2bdbe7e6ec78ce9c44a3b96f13d48b2ceea076c792ffbecd5bbbf1f"
42
42
  },
43
43
  "crow_skills/cli.py": {
44
44
  "executable": false,
45
45
  "kind": "file",
46
- "sha256": "f38bfcede958343266e1a6f3ac061e4321c04a4e264e5b96a91d93bb357b28a4"
46
+ "sha256": "3182cabd5cf63e780e37f0ae0ef5b2b1090d91debaf90d54765dc80a08e6f864"
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": "ab75772bc70758b3c3b16339dcf0ec572af358dd0378f7fc3844dbf039590563"
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": "7cbc25b39ada200e516079b81a77770e641b267cd5e5868686eb0d87a6604ea8"
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": "0f425d8d694e1103cd9e56b8b6f6332756bcb8415ca04ec55e0db2f201481807"
127
127
  },
128
128
  "package.json": {
129
129
  "executable": false,
130
130
  "kind": "file",
131
- "sha256": "62aad546f7a29256d2769ffb7741dd3911c048efa9147d70305b14aea0ad32a5"
131
+ "sha256": "64613f54bedd2d62023be224e30de2dd87bbc0ac7664ed0c49ee8e20a6454ade"
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": "23cca75e8117dd6110db7e2565b0ce0b740b8efb",
720
720
  "schema_version": 1,
721
- "version": "0.14.0"
721
+ "version": "0.16.0"
722
722
  }