@andespindola/crow-skills 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/agents.py +6 -3
- package/crow_skills/broadcast.py +6 -2
- package/crow_skills/guide.py +3 -1
- package/crow_skills/storage.py +30 -2
- package/docs/cli.md +6 -0
- package/package.json +1 -1
- package/release.json +9 -9
package/crow_skills/__init__.py
CHANGED
package/crow_skills/agents.py
CHANGED
|
@@ -212,9 +212,12 @@ def refuse_store_as_cli_dir(targets, home):
|
|
|
212
212
|
for target in targets:
|
|
213
213
|
others = [a for a in target.get("agents") or [] if a != "uniflow"]
|
|
214
214
|
if others and physical(target["path"]) == store:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
# "custom" é o rótulo interno de --dest: dizer "o destino de custom" confunde quem não
|
|
216
|
+
# passou agente nenhum. A mensagem nomeia o que o usuário realmente escreveu.
|
|
217
|
+
quem = "--dest" if others == ["custom"] else ",".join(others)
|
|
218
|
+
raise CrowError("protected_path", "O destino de " + quem + " resolve para o store do Uniflow ("
|
|
219
|
+
+ store + "), que vale para todos os CLIs que ele projeta. Use --agents uniflow "
|
|
220
|
+
"para gravar no store.")
|
|
218
221
|
|
|
219
222
|
|
|
220
223
|
def inventory(home):
|
package/crow_skills/broadcast.py
CHANGED
|
@@ -793,8 +793,12 @@ def liveness(snap, age, cfg, now=None, table=None):
|
|
|
793
793
|
if exists is False or is_zombie:
|
|
794
794
|
return None # o processo sumiu, ou é zumbi: a sessão acabou, sem esperar a janela
|
|
795
795
|
recorded = presence.get("cliStart")
|
|
796
|
-
|
|
797
|
-
|
|
796
|
+
# Um heartbeat FRESCO vence a comparação de start: `cliStart` é gravado como
|
|
797
|
+
# (agora_do_hook - etime) e a tabela recalcula (agora_do_leitor - etime), então um salto de
|
|
798
|
+
# relógio (NTP, suspensão do Mac) maior que a tolerância faria os dois divergirem e mataria
|
|
799
|
+
# uma sessão VIVA. Se ela deu sinal agora, está viva, qualquer que seja a aritmética.
|
|
800
|
+
if exists and age > CLI_REUSE_SECONDS and isinstance(start, (int, float)) \
|
|
801
|
+
and isinstance(recorded, (int, float)) and abs(start - recorded) > CLI_REUSE_SECONDS:
|
|
798
802
|
return None # o número do pid foi reciclado por outro processo; esta sessão morreu
|
|
799
803
|
if age <= cfg["stale_minutes"] * 60:
|
|
800
804
|
return "live"
|
package/crow_skills/guide.py
CHANGED
|
@@ -140,7 +140,9 @@ def protect_skill(path, home):
|
|
|
140
140
|
config (config.json, prompts, backups) stay protected. The package entry itself may
|
|
141
141
|
be a link Uniflow made; the plan decides about it, never following it."""
|
|
142
142
|
path = Path(os.path.abspath(path))
|
|
143
|
-
|
|
143
|
+
from .agents import physical
|
|
144
|
+
# Mesmo motivo: o store em dotfiles resolve, e a comparação crua nunca batia.
|
|
145
|
+
if physical(path.parent) == physical(uniflow_store(home)) and NAME.fullmatch(path.name):
|
|
144
146
|
checked(path.parent)
|
|
145
147
|
return path
|
|
146
148
|
path = checked(path)
|
package/crow_skills/storage.py
CHANGED
|
@@ -216,6 +216,22 @@ class Store:
|
|
|
216
216
|
# existe para evitar. Sem resposta única, nenhuma resposta.
|
|
217
217
|
return found[0] if len(found) == 1 else None
|
|
218
218
|
|
|
219
|
+
def retire_alias(self, target):
|
|
220
|
+
"""Retira o estado gravado sob um APELIDO deste destino, depois de gravar o canônico.
|
|
221
|
+
|
|
222
|
+
Sem isto o registro antigo ficava para trás divergente e voltava a mandar se o diretório
|
|
223
|
+
deixasse de ser link: uma skill removida ressuscitava como `missing`, com a revisão errada,
|
|
224
|
+
deixando o doctor insalubre e o update em source_mismatch. Renomeia para `.superseded`
|
|
225
|
+
em vez de apagar, para que nada se perca se o diagnóstico precisar do histórico.
|
|
226
|
+
"""
|
|
227
|
+
alias = self.aliased_state_path(target)
|
|
228
|
+
if alias is None:
|
|
229
|
+
return
|
|
230
|
+
try:
|
|
231
|
+
alias.replace(alias.with_suffix(".superseded.json"))
|
|
232
|
+
except OSError:
|
|
233
|
+
pass # nunca derruba a mutação por causa da aposentadoria do apelido
|
|
234
|
+
|
|
219
235
|
def state(self, target):
|
|
220
236
|
target = resolved(target)
|
|
221
237
|
path = checked(self.state_path(target))
|
|
@@ -354,7 +370,13 @@ class Store:
|
|
|
354
370
|
"crow-skills uninstall " + name + " --dest " + str(target) + " --yes e, se for para o "
|
|
355
371
|
"Uniflow, reinstale com --agents uniflow --global.") from exc
|
|
356
372
|
# Only the uniflow agent replaces Uniflow's import links; --dest keeps refusing links.
|
|
357
|
-
|
|
373
|
+
# physical() e não resolved(): plan() tem um local chamado `resolved` mais abaixo, que
|
|
374
|
+
# torna o nome local em toda a função e faria esta linha estourar UnboundLocalError.
|
|
375
|
+
# O store do Uniflow em dotfiles tem o caminho resolvido, e comparar com o não resolvido
|
|
376
|
+
# deixava o store inteiro não gerenciável (lacuna anterior à 0.16.0).
|
|
377
|
+
from .agents import physical as _physical
|
|
378
|
+
store_target = "uniflow" in target_info["agents"] and \
|
|
379
|
+
str(target) == _physical(uniflow_store(self.home))
|
|
358
380
|
before[key] = state
|
|
359
381
|
after[key] = json.loads(json.dumps(state))
|
|
360
382
|
selected = list(names) if names else list(state["installed"])
|
|
@@ -468,6 +490,7 @@ class Store:
|
|
|
468
490
|
os.replace(work / "new" / item["name"], dest)
|
|
469
491
|
for target, state in data["after"].items():
|
|
470
492
|
atomic_json(self.state_path(target), retargeted(state, target))
|
|
493
|
+
self.retire_alias(target)
|
|
471
494
|
data["phase"] = "committed"
|
|
472
495
|
atomic_json(journal, data)
|
|
473
496
|
except (Exception, KeyboardInterrupt):
|
|
@@ -537,6 +560,7 @@ class Store:
|
|
|
537
560
|
os.replace(backup, dest)
|
|
538
561
|
for target, state in data["before"].items():
|
|
539
562
|
atomic_json(self.state_path(target), retargeted(state, target))
|
|
563
|
+
self.retire_alias(target)
|
|
540
564
|
data["phase"] = "rolled_back"
|
|
541
565
|
atomic_json(self.root / "transactions" / (data["id"] + ".json"), data)
|
|
542
566
|
|
|
@@ -583,7 +607,11 @@ class Store:
|
|
|
583
607
|
continue
|
|
584
608
|
record = state["installed"].get(name)
|
|
585
609
|
link = target / name
|
|
586
|
-
|
|
610
|
+
from .agents import physical as _physical # local `resolved` nesta função sombreia o helper
|
|
611
|
+
# Path(), não a string crua: `physical()` devolve str, e aqui o valor é comparado
|
|
612
|
+
# com um Path (`target == store`, que seria SEMPRE falso, matando em silêncio o
|
|
613
|
+
# ramo de status external_link) e usado como caminho (`store / name`, TypeError).
|
|
614
|
+
store = Path(_physical(uniflow_store(self.home)))
|
|
587
615
|
if link.is_symlink() and target == store:
|
|
588
616
|
resolved = os.path.realpath(link)
|
|
589
617
|
managed = self.managed_target(resolved, name)
|
package/docs/cli.md
CHANGED
|
@@ -228,6 +228,12 @@ O Uniflow guarda as skills num store próprio e cria, ao subir cada CLI, links d
|
|
|
228
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.
|
|
229
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`.
|
|
230
230
|
|
|
231
|
+
#### Estado gravado sob um apelido do destino
|
|
232
|
+
|
|
233
|
+
A chave do estado é o caminho FÍSICO. Um estado escrito quando `~/.claude` ainda era diretório comum fica sob outra chave, e `Store.state()` o encontra varrendo os destinos conhecidos (`aliased_state_path`) para não perder a instalação quando você move tudo para dotfiles. Depois de gravar o estado canônico, a mutação APOSENTA o apelido, renomeando-o para `.superseded.json` em vez de apagar. Sem essa aposentadoria o registro antigo ficava divergente e voltava a mandar caso o diretório deixasse de ser link: uma skill removida ressuscitava como `missing`, com a revisão errada, e o `update` passava a recusar com `source_mismatch`. Dois apelidos para o mesmo caminho físico são ambíguos e nenhum é usado.
|
|
234
|
+
|
|
235
|
+
Consequência a conhecer, se você DESFIZER o link depois de ter mexido pelo link: o estado canônico ficou sob o caminho físico de então (o dos dotfiles) e o apelido antigo foi aposentado, então o novo local não tem registro e as skills aparecem como `unmanaged`, com o `doctor` saudável. É a resposta honesta — não existe registro para aquele caminho —, e `install --replace` ou `setup` readotam o que está lá. O comportamento anterior era pior: o registro antigo ressuscitava e reportava uma skill já removida como `missing`, com a revisão errada, deixando o `doctor` insalubre e o `update` em `source_mismatch`.
|
|
236
|
+
|
|
231
237
|
#### Exercitar um hook sem efeito colateral (`--probe`)
|
|
232
238
|
|
|
233
239
|
`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.
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"crow_skills/__init__.py": {
|
|
19
19
|
"executable": false,
|
|
20
20
|
"kind": "file",
|
|
21
|
-
"sha256": "
|
|
21
|
+
"sha256": "fb5a2f995f5007c08a1d21d9853763865628ac218330da800ef7b3461282a626"
|
|
22
22
|
},
|
|
23
23
|
"crow_skills/__main__.py": {
|
|
24
24
|
"executable": false,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"crow_skills/agents.py": {
|
|
29
29
|
"executable": false,
|
|
30
30
|
"kind": "file",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "903750fa57f9aa3cba686bcbd3775881493275b1e5aff86c5e57035d2144c7f1"
|
|
32
32
|
},
|
|
33
33
|
"crow_skills/autoupdate.py": {
|
|
34
34
|
"executable": false,
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"crow_skills/broadcast.py": {
|
|
39
39
|
"executable": false,
|
|
40
40
|
"kind": "file",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "d886806e138953dfe47bd19c8f12b4267f5137e510346081c2e3fb15e4b7a8fb"
|
|
42
42
|
},
|
|
43
43
|
"crow_skills/cli.py": {
|
|
44
44
|
"executable": false,
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"crow_skills/guide.py": {
|
|
59
59
|
"executable": false,
|
|
60
60
|
"kind": "file",
|
|
61
|
-
"sha256": "
|
|
61
|
+
"sha256": "0b5ca9d1629bcdc88e30f42b14b1189f41e9417b46c16771c339fe3ffc09dece"
|
|
62
62
|
},
|
|
63
63
|
"crow_skills/hook.py": {
|
|
64
64
|
"executable": false,
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"crow_skills/storage.py": {
|
|
104
104
|
"executable": false,
|
|
105
105
|
"kind": "file",
|
|
106
|
-
"sha256": "
|
|
106
|
+
"sha256": "2e276f27c25256508670afcd1d6da38811819351dee45e42f4098090a00d8832"
|
|
107
107
|
},
|
|
108
108
|
"crow_skills/trust.py": {
|
|
109
109
|
"executable": false,
|
|
@@ -123,12 +123,12 @@
|
|
|
123
123
|
"docs/cli.md": {
|
|
124
124
|
"executable": false,
|
|
125
125
|
"kind": "file",
|
|
126
|
-
"sha256": "
|
|
126
|
+
"sha256": "84bc06ac93083a18d81d202409dfe26202feb20d4d6cab7a46520badd2b13a13"
|
|
127
127
|
},
|
|
128
128
|
"package.json": {
|
|
129
129
|
"executable": false,
|
|
130
130
|
"kind": "file",
|
|
131
|
-
"sha256": "
|
|
131
|
+
"sha256": "774b17857c2e1701e444296d522e1d36d3a1b4915b63bb86d3bc45d405a51f73"
|
|
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": "
|
|
719
|
+
"revision": "a0d9e9a088c074f4422b2181ac0629631dbea321",
|
|
720
720
|
"schema_version": 1,
|
|
721
|
-
"version": "0.16.
|
|
721
|
+
"version": "0.16.1"
|
|
722
722
|
}
|