@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.
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/agents.py +35 -2
- package/crow_skills/autoupdate.py +17 -4
- package/crow_skills/cli.py +54 -22
- package/crow_skills/guide.py +0 -1
- package/crow_skills/hook.py +32 -14
- package/crow_skills/machine.py +143 -11
- package/crow_skills/projection.py +69 -27
- package/crow_skills/resources.py +5 -3
- package/crow_skills/storage.py +9 -1
- package/crow_skills/ui.py +1 -1
- package/docs/cli.md +23 -11
- package/package.json +1 -1
- package/release.json +15 -15
package/crow_skills/__init__.py
CHANGED
package/crow_skills/agents.py
CHANGED
|
@@ -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": (".
|
|
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
|
|
338
|
-
|
|
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
|
-
|
|
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
|
|
package/crow_skills/cli.py
CHANGED
|
@@ -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
|
-
#
|
|
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": "
|
|
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
|
|
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:
|
|
769
|
-
"
|
|
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 #
|
|
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
|
|
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
|
|
955
|
-
"message": "O Codex vê " + str(len(names_)) + " skill(s) duas vezes
|
|
956
|
-
"
|
|
957
|
-
|
|
958
|
-
|
|
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
|
|
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
|
|
1068
|
-
"por install; remova-as com o
|
|
1069
|
-
|
|
1070
|
-
|
|
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:
|
package/crow_skills/guide.py
CHANGED
|
@@ -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.
|
package/crow_skills/hook.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
760
|
-
|
|
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
|
|
858
|
-
flags.add(
|
|
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:
|
package/crow_skills/machine.py
CHANGED
|
@@ -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
|
|
286
|
-
|
|
287
|
-
managed
|
|
288
|
-
|
|
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 + ";
|
|
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
|
|
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
|
|
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
|
-
" --
|
|
441
|
+
" --dest " + str(legacy_shared_dir(home)) + " --yes"})
|
|
322
442
|
if remove:
|
|
323
443
|
cli = _cli()
|
|
324
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
"""
|
|
140
|
+
"""Copies left in the legacy shared folder (~/.agents/skills) that Codex lists a second time.
|
|
104
141
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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"]
|
package/crow_skills/resources.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/crow_skills/storage.py
CHANGED
|
@@ -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,
|
|
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 (`~/.
|
|
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 | `~/.
|
|
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
|
-
|
|
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`).
|
|
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
|
|
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
|
|
226
|
-
- `install --agents codex` não cria cópia
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
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": "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": "
|
|
31
|
+
"sha256": "0f8a284d5ce6dfc8c55a81be861c56c1884156937ecbce3a8800aef4ade76496"
|
|
32
32
|
},
|
|
33
33
|
"crow_skills/autoupdate.py": {
|
|
34
34
|
"executable": false,
|
|
35
35
|
"kind": "file",
|
|
36
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
61
|
+
"sha256": "aa8c16b293a72d6aabd99246a3666e40beaa81d5c1b77e6389341eca463e6a7f"
|
|
62
62
|
},
|
|
63
63
|
"crow_skills/hook.py": {
|
|
64
64
|
"executable": false,
|
|
65
65
|
"kind": "file",
|
|
66
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
91
|
+
"sha256": "927cabbdde134c5c504abf6e762026295e6a7a38e8bee1f45a608d21b716be4a"
|
|
92
92
|
},
|
|
93
93
|
"crow_skills/resources.py": {
|
|
94
94
|
"executable": false,
|
|
95
95
|
"kind": "file",
|
|
96
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
126
|
+
"sha256": "9dd1b9cc567e01356ead3d667b3ea559534d44930dffaa9dff7808e3476f25a3"
|
|
127
127
|
},
|
|
128
128
|
"package.json": {
|
|
129
129
|
"executable": false,
|
|
130
130
|
"kind": "file",
|
|
131
|
-
"sha256": "
|
|
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": "
|
|
719
|
+
"revision": "4297812328b55870f4b4e7a9bb3e4e7d4f82e358",
|
|
720
720
|
"schema_version": 1,
|
|
721
|
-
"version": "0.
|
|
721
|
+
"version": "0.15.0"
|
|
722
722
|
}
|