@andespindola/crow-skills 0.23.0 → 0.25.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/LICENSE +32 -4
- package/THIRD-PARTY-NOTICES.md +43 -0
- package/catalog.json +148 -38
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/autoupdate.py +6 -1
- package/crow_skills/cli.py +189 -1
- package/crow_skills/guide.py +3 -0
- package/crow_skills/integrations.py +105 -1
- package/crow_skills/storage.py +4 -0
- package/crow_skills/trust.py +73 -0
- package/docs/cli.md +63 -1
- package/package.json +2 -1
- package/release.json +202 -57
- package/skills/crow-accessibility/SKILL.md +7 -5
- package/skills/crow-accessibility/references/verificacao.md +102 -0
- package/skills/crow-agent-security/SKILL.md +8 -4
- package/skills/crow-agent-security/references/limites-agente.md +17 -0
- package/skills/crow-api-test/SKILL.md +2 -0
- package/skills/crow-api-test/references/casos-negativos.md +16 -0
- package/skills/crow-architecture/SKILL.md +2 -0
- package/skills/crow-architecture/references/modulos-profundos.md +18 -0
- package/skills/crow-backend/SKILL.md +3 -1
- package/skills/crow-backend/references/servico-backend.md +13 -0
- package/skills/crow-be-human/SKILL.md +2 -1
- package/skills/crow-be-human/references/voz-e-registro.md +35 -0
- package/skills/crow-behaviour/SKILL.md +21 -7
- package/skills/crow-behaviour/references/briefing-executavel.md +65 -0
- package/skills/crow-behaviour/references/ciclo-de-revisao.md +104 -0
- package/skills/crow-behaviour/references/micro-agents.md +52 -5
- package/skills/crow-behaviour/references/progress-ledger.md +3 -1
- package/skills/crow-behaviour/scripts/crow_micro.py +45 -0
- package/skills/crow-behaviour/scripts/micro_agent_instructions.md +15 -3
- package/skills/crow-browser/SKILL.md +45 -3
- package/skills/crow-browser/references/mecanismo-de-browser.md +24 -0
- package/skills/crow-browser/references/pagina.md +43 -0
- package/skills/crow-debug/SKILL.md +1 -1
- package/skills/crow-design/SKILL.md +6 -6
- package/skills/crow-design/references/contrato.md +84 -0
- package/skills/crow-discovery/SKILL.md +5 -4
- package/skills/crow-discovery/references/pesquisa-sem-vies.md +33 -0
- package/skills/crow-documents/SKILL.md +9 -3
- package/skills/crow-documents/references/ooxml.md +43 -0
- package/skills/crow-documents/references/pdf.md +31 -0
- package/skills/crow-documents/references/planilha.md +35 -0
- package/skills/crow-domain/SKILL.md +2 -0
- package/skills/crow-domain/references/invariante-e-identidade.md +11 -0
- package/skills/crow-frontend/SKILL.md +6 -5
- package/skills/crow-growth/SKILL.md +2 -1
- package/skills/crow-growth/references/experimento-e-metrica.md +39 -0
- package/skills/crow-infra/SKILL.md +6 -5
- package/skills/crow-infra/references/kubernetes.md +6 -0
- package/skills/crow-interface/SKILL.md +1 -1
- package/skills/crow-interface/reference/adapt.md +8 -0
- package/skills/crow-interface/reference/layout.md +4 -1
- package/skills/crow-interface/reference/workflow.md +2 -0
- package/skills/crow-media/SKILL.md +5 -5
- package/skills/crow-media/references/producao.md +63 -0
- package/skills/crow-native-mobile/SKILL.md +1 -1
- package/skills/crow-observe/SKILL.md +8 -4
- package/skills/crow-observe/references/cardinalidade.md +11 -0
- package/skills/crow-performance/SKILL.md +5 -5
- package/skills/crow-performance/references/measurement.md +6 -0
- package/skills/crow-platform/SKILL.md +4 -4
- package/skills/crow-postgres/SKILL.md +3 -0
- package/skills/crow-postgres/references/migracao-online.md +17 -0
- package/skills/crow-product-leadership/SKILL.md +4 -3
- package/skills/crow-product-leadership/references/decisao-e-pessoas.md +35 -0
- package/skills/crow-qa/SKILL.md +1 -1
- package/skills/crow-release/SKILL.md +10 -5
- package/skills/crow-release/references/transicao.md +13 -0
- package/skills/crow-reverse/SKILL.md +1 -1
- package/skills/crow-secrets/SKILL.md +2 -2
- package/skills/crow-security-review/SKILL.md +12 -5
- package/skills/crow-security-scan/SKILL.md +8 -3
- package/skills/crow-security-scan/references/evidencia-scan.md +17 -0
- package/skills/crow-security-test/SKILL.md +5 -1
- package/skills/crow-security-test/references/prova-dinamica.md +24 -0
- package/skills/crow-seo/SKILL.md +6 -4
- package/skills/crow-seo/references/saida-tecnica.md +78 -0
- package/skills/crow-skill-author/SKILL.md +10 -6
- package/skills/crow-skill-author/references/medicao.md +85 -0
- package/skills/crow-spec/SKILL.md +4 -3
- package/skills/crow-spec/references/criterio-e-fatia.md +27 -0
- package/skills/crow-threat-model/SKILL.md +5 -1
- package/skills/crow-threat-model/references/ciclo-ameaca.md +15 -0
package/crow_skills/cli.py
CHANGED
|
@@ -48,6 +48,9 @@ COMMANDS = {
|
|
|
48
48
|
"status": ("Instalações, guias, integrações, modo e autoupdate, por agente.", ["crow-skills status", "crow-skills status --agents claude"]),
|
|
49
49
|
"doctor": ("Como status, com healthy true/false: skills editadas/ausentes, guia alterado, hooks não aprovados, transação pendente.",
|
|
50
50
|
["crow-skills doctor", "crow-skills doctor --json"]),
|
|
51
|
+
"skills": ("Versão de cada skill instalada e se está na revisão da fonte atual (latest) ou atrasada; somente leitura.",
|
|
52
|
+
["crow-skills skills", "crow-skills skills --outdated", "crow-skills skills --json",
|
|
53
|
+
"crow-skills skills --agents claude --check-npm"]),
|
|
51
54
|
"info": ("Detalhes de uma skill do catálogo.", ["crow-skills info crow-backend"]),
|
|
52
55
|
"related": ("Handoffs, overlays e disponibilidade de complementos de uma skill.", ["crow-skills related crow-backend --agents claude --global"]),
|
|
53
56
|
"install": ("Instala skills escolhidas em agentes e escopo explícitos, com preview, backup e transação.",
|
|
@@ -151,6 +154,10 @@ def parser():
|
|
|
151
154
|
child.add_argument("topic", nargs="?", help="Comando, ou flags")
|
|
152
155
|
for name in ("list", "groups", "agents", "status", "doctor"):
|
|
153
156
|
command_parser(sub, name, [common])
|
|
157
|
+
child = command_parser(sub, "skills", [common])
|
|
158
|
+
child.add_argument("--outdated", action="store_true", help="Só as skills que não estão na revisão da fonte atual")
|
|
159
|
+
child.add_argument("--check-npm", dest="check_npm", action="store_true",
|
|
160
|
+
help="Também consultar o npm se o crow-skills instalado é a última versão publicada (única chamada de rede)")
|
|
154
161
|
for name in ("info", "related"):
|
|
155
162
|
child = command_parser(sub, name, [common])
|
|
156
163
|
child.add_argument("name")
|
|
@@ -978,6 +985,173 @@ def group_coverage(rows):
|
|
|
978
985
|
for v in by_target.values() if v["names"]]
|
|
979
986
|
|
|
980
987
|
|
|
988
|
+
# Estado de versão de uma skill instalada, contra a fonte deste crow-skills. "latest" é o único
|
|
989
|
+
# estado em dia; os demais ou pedem conserto (update/install) ou não são instalação gerenciada.
|
|
990
|
+
SKILL_STATE_WORDS = {
|
|
991
|
+
"latest": "igual à fonte atual",
|
|
992
|
+
"desatualizada": "revisão/origem diferente da fonte atual (crow-skills update)",
|
|
993
|
+
"modificada": "editada no disco",
|
|
994
|
+
"desativada": "instalada, mas desabilitada",
|
|
995
|
+
"ausente": "registrada e não encontrada no disco",
|
|
996
|
+
"via_uniflow": "projetada do seu store do Uniflow (gerenciada em --agents uniflow)",
|
|
997
|
+
"link_externo": "link no store do Uniflow para fora dele",
|
|
998
|
+
"nao_gerenciada": "não gerenciada por este CLI",
|
|
999
|
+
"indeterminada": "não foi possível ler o destino",
|
|
1000
|
+
}
|
|
1001
|
+
SKILL_PROBLEM_STATES = ("desatualizada", "modificada", "ausente")
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def skill_state(row, source):
|
|
1005
|
+
"""Classifica UMA linha de store.status contra `source`. Regra única, usada por `skills`,
|
|
1006
|
+
`status` e `doctor` — a visibilidade e o veredito nunca podem discordar."""
|
|
1007
|
+
status = row.get("status")
|
|
1008
|
+
if status == "installed":
|
|
1009
|
+
if row.get("revision") == source.revision and row.get("source") == str(source.path):
|
|
1010
|
+
return "latest"
|
|
1011
|
+
return "desatualizada"
|
|
1012
|
+
if status == "modified":
|
|
1013
|
+
return "modificada"
|
|
1014
|
+
if status == "disabled":
|
|
1015
|
+
return "desativada"
|
|
1016
|
+
if status == "missing":
|
|
1017
|
+
return "ausente"
|
|
1018
|
+
if status == "uniflow_link":
|
|
1019
|
+
# Projeção SAUDÁVEL do store do Uniflow: é a topologia normal de quem instala em uniflow e
|
|
1020
|
+
# deixa os CLIs lerem de lá, e o resto do CLI já a conta como instalada (group_coverage).
|
|
1021
|
+
# Chamar isso de "não gerenciada" assustava sem motivo — 33 skills em 4 agentes, aqui.
|
|
1022
|
+
# Projeção quebrada, desabilitada ou para pacote não gerenciado não chega como uniflow_link:
|
|
1023
|
+
# unmanaged_projections() a reescreve antes (modified/disabled/unmanaged).
|
|
1024
|
+
return "via_uniflow"
|
|
1025
|
+
if status == "external_link":
|
|
1026
|
+
return "link_externo"
|
|
1027
|
+
if status in ("unmanaged", "absent"):
|
|
1028
|
+
return "nao_gerenciada"
|
|
1029
|
+
return "indeterminada" # "unsafe" e qualquer estado novo: nunca contado como em dia
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def outdated_rows(rows, source):
|
|
1033
|
+
"""Só as linhas instaladas atrasadas em relação à fonte: o que `status`/`doctor` precisam avisar."""
|
|
1034
|
+
return [r for r in rows if r.get("name") and skill_state(r, source) == "desatualizada"]
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def outdated_warning(rows, source):
|
|
1038
|
+
"""Aviso skills_outdated (ou None). Mesmo texto em status e doctor."""
|
|
1039
|
+
outdated = outdated_rows(rows, source)
|
|
1040
|
+
if not outdated:
|
|
1041
|
+
return None
|
|
1042
|
+
names = sorted({r["name"] for r in outdated})
|
|
1043
|
+
agents = sorted({a for r in outdated for a in (r.get("agents") or [])})
|
|
1044
|
+
return {"code": "skills_outdated", "skills": names, "agents": agents,
|
|
1045
|
+
"message": str(len(names)) + " skill(s) instalada(s) estão atrasadas em relação à fonte atual ("
|
|
1046
|
+
+ short_revision(source.revision) + " em " + str(source.path) + "): "
|
|
1047
|
+
+ ", ".join(names) + ". Veja detalhes com crow-skills skills --outdated.",
|
|
1048
|
+
"fix": fix_command(agents)}
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def fix_command(agents):
|
|
1052
|
+
"""O comando exato que conserta: o mesmo texto no rodapé de `skills` e no aviso de status/doctor."""
|
|
1053
|
+
return "crow-skills update --all --agents " + (",".join(agents) or ",".join(AGENTS)) + " --global --yes"
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
def short_revision(revision):
|
|
1057
|
+
return (str(revision)[:12] if revision else "—")
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def npm_latest():
|
|
1061
|
+
"""Última versão publicada do pacote npm, ou um motivo. Única chamada de rede do comando;
|
|
1062
|
+
sem npm ou sem rede devolve reason e NUNCA inventa um número."""
|
|
1063
|
+
import shutil
|
|
1064
|
+
import subprocess
|
|
1065
|
+
npm = shutil.which("npm")
|
|
1066
|
+
if not npm:
|
|
1067
|
+
return {"checked": False, "latest": None, "reason": "npm não encontrado no PATH"}
|
|
1068
|
+
try:
|
|
1069
|
+
done = subprocess.run([npm, "view", autoupdate.PACKAGE, "version"], capture_output=True, text=True,
|
|
1070
|
+
timeout=20, stdin=subprocess.DEVNULL, errors="replace")
|
|
1071
|
+
except (OSError, subprocess.SubprocessError):
|
|
1072
|
+
return {"checked": False, "latest": None, "reason": "a consulta ao npm falhou ou excedeu o tempo"}
|
|
1073
|
+
latest = (done.stdout or "").strip().splitlines()
|
|
1074
|
+
if done.returncode != 0 or not latest:
|
|
1075
|
+
return {"checked": False, "latest": None, "reason": "o npm não respondeu uma versão"}
|
|
1076
|
+
return {"checked": True, "latest": ui.clean(latest[-1].strip()), "reason": None}
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def skills_command(args, home, store):
|
|
1080
|
+
"""Somente leitura: versão e estado de cada skill instalada, contra a fonte atual."""
|
|
1081
|
+
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"))
|
|
1082
|
+
targets = scope(args, home, read=True)
|
|
1083
|
+
# A mesma correção que status/doctor aplicam: projeção do Uniflow quebrada, desabilitada ou para
|
|
1084
|
+
# pacote não gerenciado não pode passar por projeção saudável.
|
|
1085
|
+
rows = unmanaged_projections(store, home, store.status(targets))
|
|
1086
|
+
items = {}
|
|
1087
|
+
for row in rows:
|
|
1088
|
+
if not row.get("name"):
|
|
1089
|
+
continue
|
|
1090
|
+
state = skill_state(row, source)
|
|
1091
|
+
# Uma skill é UMA linha; agentes se agregam. Estados divergentes entre agentes viram
|
|
1092
|
+
# linhas separadas, porque um "latest" não pode esconder um "desatualizada" ao lado.
|
|
1093
|
+
item = items.setdefault((row["name"], state),
|
|
1094
|
+
{"name": row["name"], "state": state, "agents": [], "revisions": []})
|
|
1095
|
+
for agent in row.get("agents") or []:
|
|
1096
|
+
if agent not in item["agents"]:
|
|
1097
|
+
item["agents"].append(agent)
|
|
1098
|
+
if row.get("revision") and row["revision"] not in item["revisions"]:
|
|
1099
|
+
item["revisions"].append(row["revision"])
|
|
1100
|
+
skills = []
|
|
1101
|
+
for key in sorted(items):
|
|
1102
|
+
item = items[key]
|
|
1103
|
+
skills.append({"name": item["name"], "agents": sorted(item["agents"]), "state": item["state"],
|
|
1104
|
+
"meaning": SKILL_STATE_WORDS[item["state"]],
|
|
1105
|
+
"revision": item["revisions"][0] if len(item["revisions"]) == 1 else None,
|
|
1106
|
+
"revisions": sorted(item["revisions"])})
|
|
1107
|
+
shown = [s for s in skills if s["state"] != "latest"] if getattr(args, "outdated", False) else skills
|
|
1108
|
+
counts = {state: len([s for s in skills if s["state"] == state]) for state in SKILL_PROBLEM_STATES}
|
|
1109
|
+
affected = sorted({a for s in skills if s["state"] in SKILL_PROBLEM_STATES for a in s["agents"]})
|
|
1110
|
+
data = {"version": __version__, "source": str(source.path), "revision": source.revision,
|
|
1111
|
+
"skills": shown, "counts": counts, "outdated_only": bool(getattr(args, "outdated", False)),
|
|
1112
|
+
"fix": fix_command(affected) if any(counts.values()) else None}
|
|
1113
|
+
if getattr(args, "check_npm", False):
|
|
1114
|
+
data["npm"] = {**npm_latest(), "installed": __version__}
|
|
1115
|
+
if data["npm"]["checked"]:
|
|
1116
|
+
data["npm"]["up_to_date"] = data["npm"]["latest"] == __version__
|
|
1117
|
+
return data
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def render_skills(data, stream=None):
|
|
1121
|
+
stream = stream or sys.stdout
|
|
1122
|
+
lines = [ui.paint("crow-skills " + data["version"], "bold", stream) + " · fonte " + ui.clean(data["source"]) +
|
|
1123
|
+
" · revisão " + short_revision(data["revision"]), ""]
|
|
1124
|
+
if data["skills"]:
|
|
1125
|
+
table = [[s["name"], ",".join(s["agents"]),
|
|
1126
|
+
short_revision(s["revision"]) if s["revision"] else
|
|
1127
|
+
(",".join(short_revision(r) for r in s["revisions"]) or "—"),
|
|
1128
|
+
s["state"]] for s in data["skills"]]
|
|
1129
|
+
lines += ui.table(table, ["skill", "agentes", "revisão", "estado"], stream)
|
|
1130
|
+
elif data["outdated_only"]:
|
|
1131
|
+
lines.append(" " + ui.mark("ok", stream) + " Nenhuma skill fora da revisão da fonte atual.")
|
|
1132
|
+
else:
|
|
1133
|
+
lines.append(" Nenhuma skill instalada; comece com: crow-skills setup --yes")
|
|
1134
|
+
npm = data.get("npm")
|
|
1135
|
+
if npm:
|
|
1136
|
+
if not npm["checked"]:
|
|
1137
|
+
lines += ["", " npm: não foi possível conferir a última versão publicada (" + ui.clean(npm["reason"]) + ")."]
|
|
1138
|
+
elif npm.get("up_to_date"):
|
|
1139
|
+
lines += ["", " " + ui.mark("ok", stream) + " npm: " + npm["latest"] + " é a última publicada."]
|
|
1140
|
+
else:
|
|
1141
|
+
lines += ["", " " + ui.mark("warn", stream) + " npm: instalado " + npm["installed"] + ", publicado " +
|
|
1142
|
+
npm["latest"] + ".\n Para corrigir: crow-skills upgrade --yes"]
|
|
1143
|
+
counts = data["counts"]
|
|
1144
|
+
if any(counts.values()):
|
|
1145
|
+
lines += ["", ui.mark("warn", stream) + " " +
|
|
1146
|
+
", ".join(str(counts[s]) + " " + s + ("s" if counts[s] != 1 else "")
|
|
1147
|
+
for s in SKILL_PROBLEM_STATES if counts[s]) +
|
|
1148
|
+
"\n Para corrigir: " + data["fix"]]
|
|
1149
|
+
if counts["modificada"] or counts["ausente"]:
|
|
1150
|
+
# O comando acima não toca o que foi editado/removido sem autorização explícita.
|
|
1151
|
+
lines.append(" Skill editada ou ausente só é substituída acrescentando --overwrite-modified.")
|
|
1152
|
+
return "\n".join(lines)
|
|
1153
|
+
|
|
1154
|
+
|
|
981
1155
|
def groups_command(args):
|
|
982
1156
|
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"), getattr(args, "fetch", False))
|
|
983
1157
|
available = groups_mod.load(source)
|
|
@@ -1046,6 +1220,8 @@ def run(args):
|
|
|
1046
1220
|
return data
|
|
1047
1221
|
if args.command == "autoupdate":
|
|
1048
1222
|
return autoupdate_command(args, home, store, interactive)
|
|
1223
|
+
if args.command == "skills":
|
|
1224
|
+
return skills_command(args, home, store)
|
|
1049
1225
|
if args.command in ("status", "doctor"):
|
|
1050
1226
|
targets = scope(args, home, read=True)
|
|
1051
1227
|
rows = unmanaged_projections(store, home, store.status(targets))
|
|
@@ -1086,6 +1262,13 @@ def run(args):
|
|
|
1086
1262
|
"Codex continua carregando de lá.",
|
|
1087
1263
|
"fix": "crow-skills setup --agents codex --yes (instala no diretório do Codex e migra), ou "
|
|
1088
1264
|
"crow-skills uninstall " + ",".join(leftovers) + " --dest " + str(legacy_shared_dir(home)) + " --yes"})
|
|
1265
|
+
try:
|
|
1266
|
+
current = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"))
|
|
1267
|
+
except (CrowError, OSError):
|
|
1268
|
+
current = None # sem fonte legível não há com o que comparar; nenhum aviso inventado
|
|
1269
|
+
outdated = outdated_warning(rows, current) if current is not None else None
|
|
1270
|
+
if outdated:
|
|
1271
|
+
result.setdefault("warnings", []).append(outdated)
|
|
1089
1272
|
result["groups"] = group_coverage(rows)
|
|
1090
1273
|
try:
|
|
1091
1274
|
result["autoupdate"] = autoupdate.status(store, home) if global_view else None
|
|
@@ -1095,7 +1278,10 @@ def run(args):
|
|
|
1095
1278
|
result["hooks"] = [{"integration": r["integration"], "agents": r.get("agents"), "state": r["enabled"].get("state"),
|
|
1096
1279
|
"fix": r["enabled"].get("fix")} for r in hooks]
|
|
1097
1280
|
if args.command == "doctor":
|
|
1098
|
-
|
|
1281
|
+
# Uma skill instalada atrasada em relação à fonte não é "healthy": o doctor existe para
|
|
1282
|
+
# dizer que falta rodar o update, não para dar ✓ com pacote velho no disco.
|
|
1283
|
+
result.update(healthy=not pending and not outdated
|
|
1284
|
+
and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides)
|
|
1099
1285
|
and (result["autoupdate"] or {}).get("healthy", True) is not False
|
|
1100
1286
|
and all(h["state"] not in machine.BAD_HOOKS for h in result["hooks"]),
|
|
1101
1287
|
agents=inventory(home))
|
|
@@ -1300,6 +1486,8 @@ def emit(command, data, as_json):
|
|
|
1300
1486
|
print(machine.render_upgrade(data))
|
|
1301
1487
|
elif command in ("status", "doctor"):
|
|
1302
1488
|
print(render_status(data, doctor=command == "doctor"))
|
|
1489
|
+
elif command == "skills":
|
|
1490
|
+
print(render_skills(data))
|
|
1303
1491
|
elif command == "progress":
|
|
1304
1492
|
if data is not None:
|
|
1305
1493
|
print(data["text"] or "crow: nenhuma execução registrada neste diretório.")
|
package/crow_skills/guide.py
CHANGED
|
@@ -256,6 +256,9 @@ def render_body(agent, catalog, revision, skills):
|
|
|
256
256
|
"- Qualquer pedido que não seja conversa pura, pergunta factual curta ou ajuste trivial e local — inclusive quando é um arquivo só, uma decisão só ou só um plano: carregue a skill crow-behaviour ANTES de começar, por iniciativa própria, sem esperar pedido. Para carregar, " + mech["load"] + "; por exemplo, " + invoke(name="crow-behaviour") + ". Siga-a até a entrega.",
|
|
257
257
|
"- Vale também quando a entrega é só um plano, um diagnóstico ou uma revisão, ou quando não há código no diretório: depurar, auditar, investigar, migrar ou planejar uma mudança continua entrando. Na dúvida, carregue.",
|
|
258
258
|
"- Uma vez ativa, ela segue em uso nos prompts seguintes da sessão: mesmo plano, mesmo ledger e mesma validação por fases, e cada novo pedido entra como unidade nova em vez de recomeçar sem registro. Se perceber que parou de mantê-los, retome na mesma resposta, registre a lacuna e siga — sem pedir permissão.",
|
|
259
|
+
"- Com ela ativa, o braçal é delegado por padrão: unidade cujo resultado você consegue especificar e aceitar pela evidência vai para um micro-agent de escopo mínimo e focado, com uma saída e nada \"enquanto estiver lá\". Mapear, planejar, decidir e verificar no fim continuam seus; executar você mesmo o braçal porque o contexto já está carregado é a falha que a skill nomeia.",
|
|
260
|
+
"- O micro-agent não recebe responsabilidade além de executar o que foi delegado: não grava memória nem notas, não mantém nada em sincronia, não mexe em git, não avisa outras sessões e não amplia o escopo — ele informa tudo a você no retorno (o que mudou, a saída real, o que não verificou, o que falhou, o que ficou pela metade).",
|
|
261
|
+
"- Salvar na memória durável e manter tudo em sincronia é seu e não se delega, e o registro acontece à medida que os retornos chegam, não num mutirão no fim: o retorno do executor é a única fonte do registro, e retorno não registrado é trabalho que ninguém sabe que foi feito.",
|
|
259
262
|
"- Conversa, pergunta factual curta ou ajuste trivial e local: responda direto, sem carregá-la.",
|
|
260
263
|
"",
|
|
261
264
|
"## Skills especialistas",
|
|
@@ -344,6 +344,53 @@ GEMINI_EVENTS = ["SessionStart", "BeforeAgent", "BeforeTool", "AfterTool", "Afte
|
|
|
344
344
|
AGY_EVENTS = ["SessionStart", "PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"]
|
|
345
345
|
DEVIN_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PermissionRequest", "SubagentStop", "Stop",
|
|
346
346
|
"SessionEnd"]
|
|
347
|
+
_DEVIN_ACCEPTED = {}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def devin_accepted():
|
|
351
|
+
"""(eventos aceitos | None, motivo da falha): a sonda do devin, uma vez por binário no processo."""
|
|
352
|
+
binary = hooktrust.devin_binary()
|
|
353
|
+
# Pelo caminho RESOLVIDO: o devin se atualiza repontando ~/.local/bin/devin -> _versions/current,
|
|
354
|
+
# e uma imagem com mtimes normalizados (OCI, Nix) não mudaria a chave. Sem exists() antes do
|
|
355
|
+
# getmtime: entre os dois o symlink pode ser repontado, e aí a resposta é "não deu para perguntar".
|
|
356
|
+
try:
|
|
357
|
+
resolved = os.path.realpath(binary) if binary else None
|
|
358
|
+
key = (resolved, os.path.getmtime(resolved) if resolved else None)
|
|
359
|
+
except OSError as exc:
|
|
360
|
+
return None, "o binário do devin mudou enquanto era consultado (" + exc.__class__.__name__ + ")"
|
|
361
|
+
if key not in _DEVIN_ACCEPTED:
|
|
362
|
+
try:
|
|
363
|
+
_DEVIN_ACCEPTED[key] = (list(hooktrust.devin_hook_events(binary)), None)
|
|
364
|
+
except hooktrust.ProbeError as exc:
|
|
365
|
+
_DEVIN_ACCEPTED[key] = (None, str(exc))
|
|
366
|
+
return _DEVIN_ACCEPTED[key]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def devin_events():
|
|
370
|
+
"""(events, note): the DEVIN_EVENTS this devin really accepts, plus what was dropped and why.
|
|
371
|
+
|
|
372
|
+
devin discards its ENTIRE hooks block when it rejects a single event, so an event this build
|
|
373
|
+
does not know would silently turn off every crow hook while the config still looked installed.
|
|
374
|
+
The accepted list is read from devin (trust.devin_hook_events); when devin cannot be asked, the
|
|
375
|
+
conservative baseline is written instead of guessing."""
|
|
376
|
+
accepted, failure = devin_accepted()
|
|
377
|
+
allowed = accepted if accepted is not None else list(hooktrust.DEVIN_BASELINE)
|
|
378
|
+
events = [e for e in DEVIN_EVENTS if e in allowed]
|
|
379
|
+
dropped = [e for e in DEVIN_EVENTS if e not in allowed]
|
|
380
|
+
if not events and accepted is not None:
|
|
381
|
+
# Nenhum evento em comum. O baseline NÃO serve de plano B aqui: seriam eventos que este
|
|
382
|
+
# devin recusa, e um evento recusado derruba o bloco inteiro. Melhor não gravar nada.
|
|
383
|
+
return [], ("Este devin não aceita nenhum dos eventos que o crow-skills usa (aceita: "
|
|
384
|
+
+ ", ".join(accepted) + "); os hooks não foram gravados, porque um evento recusado "
|
|
385
|
+
"faz o devin descartar o bloco hooks inteiro.")
|
|
386
|
+
note = None
|
|
387
|
+
if dropped and accepted is not None:
|
|
388
|
+
note = ("Este devin não conhece " + ", ".join(dropped) + "; os eventos foram omitidos. Um evento desconhecido "
|
|
389
|
+
"faz o devin descartar o bloco hooks inteiro, então todos os hooks do crow-skills ficariam sem rodar.")
|
|
390
|
+
elif dropped:
|
|
391
|
+
note = ("Não foi possível perguntar ao devin quais eventos ele aceita (" + (failure or "motivo desconhecido")
|
|
392
|
+
+ "); escritos só os eventos que todo devin conhecido aceita, sem " + ", ".join(dropped) + ".")
|
|
393
|
+
return events, note
|
|
347
394
|
AGY_KEY = "crow-skills"
|
|
348
395
|
|
|
349
396
|
|
|
@@ -410,7 +457,7 @@ def hook_entries(store, home, python, host):
|
|
|
410
457
|
if host == "agy":
|
|
411
458
|
return {e: ([{"matcher": "", "hooks": [{"type": "command", "command": command(e), "timeout": 3}]}] if e in AGY_GROUPED
|
|
412
459
|
else [{"type": "command", "command": command(e), "timeout": 3}]) for e in AGY_EVENTS}
|
|
413
|
-
return {e: [{"hooks": [{"type": "command", "command": command(e)}]}] for e in
|
|
460
|
+
return {e: [{"hooks": [{"type": "command", "command": command(e)}]}] for e in devin_events()[0]}
|
|
414
461
|
|
|
415
462
|
|
|
416
463
|
def is_ours(entry, store):
|
|
@@ -540,6 +587,11 @@ def hooks_sync(name, store, home, record, desired, python, dry):
|
|
|
540
587
|
return None, "remove", None
|
|
541
588
|
if not quoted_ok(python, hook_script_path(store), home):
|
|
542
589
|
return record, "skip", "Caminho do Python, do estado ou do home contém aspas/variáveis; hooks não configurados."
|
|
590
|
+
if spec["host"] == "devin":
|
|
591
|
+
events, devin_note = devin_events()
|
|
592
|
+
if not events:
|
|
593
|
+
# Gravar um bloco vazio diria "instalado" sem hook nenhum, e a nota se perderia.
|
|
594
|
+
return (record if desired else None), "skip", devin_note
|
|
543
595
|
entries = hook_entries(store, home, python, spec["host"])
|
|
544
596
|
wanted = refresh_in_place(data, entries, store, spec["layout"]) or add_hooks(stripped, entries, spec["layout"])
|
|
545
597
|
if wanted == data:
|
|
@@ -552,6 +604,8 @@ def hooks_sync(name, store, home, record, desired, python, dry):
|
|
|
552
604
|
note = None
|
|
553
605
|
if spec["host"] == "codex":
|
|
554
606
|
note = (codex_approval_note() + codex_hooks_feature(home)).strip() or None
|
|
607
|
+
elif spec["host"] == "devin":
|
|
608
|
+
note = devin_events()[1]
|
|
555
609
|
if dry:
|
|
556
610
|
return new_record, action, note
|
|
557
611
|
tracked_write(store, path, dump(wanted), old, spec["backup"])
|
|
@@ -1278,9 +1332,59 @@ def sync(store, home, wanted, dry_run=False, python=None, trust=True):
|
|
|
1278
1332
|
return report
|
|
1279
1333
|
|
|
1280
1334
|
|
|
1335
|
+
def devin_hooks_state(home, probe):
|
|
1336
|
+
"""Whether devin will run the hooks written in its config: {state, fix?}.
|
|
1337
|
+
|
|
1338
|
+
One event devin does not know makes it discard the whole hooks block, so a config that looks
|
|
1339
|
+
installed can run nothing. Reported instead of a green tick; the events in the file are
|
|
1340
|
+
compared with the list devin itself gives."""
|
|
1341
|
+
base = hooktrust.non_codex_state("devin", home)
|
|
1342
|
+
path = devin_dir(home) / "config.json"
|
|
1343
|
+
try:
|
|
1344
|
+
_, data, problem = read_json_object(protect(path, home), "~/.config/devin/config.json")
|
|
1345
|
+
except CrowError as exc:
|
|
1346
|
+
# Arquivo irregular (symlink, hard link, ilegível): antes isto estourava daqui e o chamador
|
|
1347
|
+
# engolia a exceção, derrubando o relatório de integrações INTEIRO e ainda imprimindo healthy.
|
|
1348
|
+
return dict(base, state="unknown", fix="~/.config/devin/config.json não pôde ser lido (" + str(exc) + ").")
|
|
1349
|
+
if problem:
|
|
1350
|
+
return dict(base, state="unknown", fix=problem)
|
|
1351
|
+
if "hooks" not in data:
|
|
1352
|
+
return base
|
|
1353
|
+
block = data.get("hooks")
|
|
1354
|
+
if not isinstance(block, dict):
|
|
1355
|
+
# O devin exige um mapa: "invalid type: sequence, expected a map" e descarta o bloco todo.
|
|
1356
|
+
return {"state": "feature_off",
|
|
1357
|
+
"fix": ("A chave hooks de ~/.config/devin/config.json não é um objeto JSON; o devin descarta o bloco "
|
|
1358
|
+
"inteiro e nenhum hook roda. Corrija o arquivo e rode crow-skills integrations install.")}
|
|
1359
|
+
if not block:
|
|
1360
|
+
return base
|
|
1361
|
+
written = list(block)
|
|
1362
|
+
if not probe:
|
|
1363
|
+
# Sem sonda dá para responder offline o caso comum: tudo o que está escrito é do conjunto
|
|
1364
|
+
# que todo devin conhecido aceita. Só o que está fora disso precisa perguntar ao devin.
|
|
1365
|
+
outside = [e for e in written if e not in hooktrust.DEVIN_BASELINE]
|
|
1366
|
+
if not outside:
|
|
1367
|
+
return base
|
|
1368
|
+
return dict(base, state="unknown",
|
|
1369
|
+
fix=("Evento(s) fora do conjunto que todo devin aceita (" + ", ".join(outside)
|
|
1370
|
+
+ "); rode crow-skills doctor para conferir com o devin instalado."))
|
|
1371
|
+
accepted, failure = devin_accepted() # mesma sonda em cache: instalar e relatar não paga duas vezes
|
|
1372
|
+
if accepted is None:
|
|
1373
|
+
return dict(base, state="unknown", fix="Não foi possível perguntar ao devin quais eventos ele aceita (" + str(failure) + ").")
|
|
1374
|
+
foreign = [e for e in written if e not in accepted]
|
|
1375
|
+
if foreign:
|
|
1376
|
+
return {"state": "feature_off",
|
|
1377
|
+
"fix": ("~/.config/devin/config.json tem evento(s) que este devin não conhece (" + ", ".join(foreign)
|
|
1378
|
+
+ "); ele descarta o bloco hooks INTEIRO, então nenhum hook roda. Rode crow-skills integrations install "
|
|
1379
|
+
"para regravar só os eventos aceitos (ou remova os eventos que não são do crow-skills).")}
|
|
1380
|
+
return base
|
|
1381
|
+
|
|
1382
|
+
|
|
1281
1383
|
def hooks_enabled(name, store, home, record, probe=True):
|
|
1282
1384
|
"""Whether the CLI will actually run the crow hooks: {state, fix?} (see trust.py)."""
|
|
1283
1385
|
host = HOOKS[name]["host"]
|
|
1386
|
+
if host == "devin":
|
|
1387
|
+
return devin_hooks_state(home, probe)
|
|
1284
1388
|
if host != "codex":
|
|
1285
1389
|
return hooktrust.non_codex_state(host, home)
|
|
1286
1390
|
base = {}
|
package/crow_skills/storage.py
CHANGED
|
@@ -638,5 +638,9 @@ class Store:
|
|
|
638
638
|
status = "unsafe"
|
|
639
639
|
rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
|
|
640
640
|
"status": status, "revision": record.get("revision") if record else None,
|
|
641
|
+
# Origem e digest do pacote registrado: é o que permite dizer "atrasada em
|
|
642
|
+
# relação a esta fonte" sem reabrir o pacote (a mesma regra de `plan`).
|
|
643
|
+
"package_sha256": record.get("package_sha256") if record else None,
|
|
644
|
+
"source": record.get("source") if record else None,
|
|
641
645
|
"activation_verified": False})
|
|
642
646
|
return rows
|
package/crow_skills/trust.py
CHANGED
|
@@ -24,10 +24,12 @@ reported from their own feature flags.
|
|
|
24
24
|
"""
|
|
25
25
|
import json
|
|
26
26
|
import os
|
|
27
|
+
import pathlib
|
|
27
28
|
import queue
|
|
28
29
|
import re
|
|
29
30
|
import shutil
|
|
30
31
|
import subprocess
|
|
32
|
+
import tempfile
|
|
31
33
|
import threading
|
|
32
34
|
import time
|
|
33
35
|
|
|
@@ -50,6 +52,77 @@ def codex_binary():
|
|
|
50
52
|
return shutil.which("codex")
|
|
51
53
|
|
|
52
54
|
|
|
55
|
+
# The hook events every devin build known to the project accepts (measured on 3000.11.3, the
|
|
56
|
+
# oldest installed here). Used only when devin cannot be asked; never a claim about the build
|
|
57
|
+
# in front of us, which is what devin_hook_events() goes and reads.
|
|
58
|
+
DEVIN_BASELINE = ("PreToolUse", "PostToolUse", "UserPromptSubmit", "Stop", "PostCompaction",
|
|
59
|
+
"SessionStart", "SessionEnd", "PermissionRequest")
|
|
60
|
+
DEVIN_PROBE_EVENT = "CrowSkillsCapabilityProbe"
|
|
61
|
+
# Measured: `devin doctor` answers in 0.56 s. This sits on the install path, so the ceiling is
|
|
62
|
+
# short on purpose — a wedged devin costs a conservative fallback, not a stalled install.
|
|
63
|
+
DEVIN_PROBE_TIMEOUT = 8.0
|
|
64
|
+
# Anchored on THREE things, because devin words every rejected enum the same way and emits the
|
|
65
|
+
# others first (theme_mode, ProxyMode, UnicodeMode): the key must be "hooks", the refused variant
|
|
66
|
+
# must be our probe event, and the list must end at devin's own ". Using the default", so a line
|
|
67
|
+
# that was wrapped, truncated or colorized does not match at all. Not matching is a ProbeError and
|
|
68
|
+
# a conservative fallback; a PARTIAL match would be a short list written as if complete, which is
|
|
69
|
+
# the silent-disable this whole probe exists to prevent.
|
|
70
|
+
_DEVIN_EXPECTED = re.compile(
|
|
71
|
+
r'invalid value for "hooks" in [^\n]*?: unknown variant `(?P<refused>[^`\n]*)`, '
|
|
72
|
+
r'expected one of (?P<accepted>`[^`\n]+`(?:, `[^`\n]+`)*)\. Using the default')
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def devin_binary():
|
|
76
|
+
"""The devin executable, or None. CROW_SKILLS_DEVIN overrides PATH (empty: treat as absent)."""
|
|
77
|
+
override = os.environ.get("CROW_SKILLS_DEVIN")
|
|
78
|
+
if override is not None:
|
|
79
|
+
return override if override and os.path.isfile(override) and os.access(override, os.X_OK) else None
|
|
80
|
+
return shutil.which("devin")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def devin_hook_events(binary=None, timeout=None):
|
|
84
|
+
"""The hook events the installed devin accepts, asked of devin itself.
|
|
85
|
+
|
|
86
|
+
devin validates config.json's `hooks` against its own enum and, rejecting one event, names
|
|
87
|
+
every event it knows: "unknown variant `X`, expected one of `A`, `B`, ...". It also drops the
|
|
88
|
+
WHOLE hooks block when it rejects a single event ("Using the default ({})"), so writing an
|
|
89
|
+
event this build does not know silently disables every crow hook. One deliberately invalid
|
|
90
|
+
event in a throwaway config directory turns that refusal into a capability list; the probe
|
|
91
|
+
pins XDG_CONFIG_HOME at the throwaway, so the user's ~/.config/devin/config.json is never
|
|
92
|
+
read and never written.
|
|
93
|
+
|
|
94
|
+
Raises ProbeError with a short reason (never devin's whole output)."""
|
|
95
|
+
binary = binary or devin_binary()
|
|
96
|
+
timeout = DEVIN_PROBE_TIMEOUT if timeout is None else timeout
|
|
97
|
+
if not binary:
|
|
98
|
+
raise ProbeError("devin não encontrado no PATH")
|
|
99
|
+
with tempfile.TemporaryDirectory(prefix="crow-devin-probe-") as temp:
|
|
100
|
+
config = pathlib.Path(temp) / "devin"
|
|
101
|
+
config.mkdir()
|
|
102
|
+
(config / "config.json").write_text(json.dumps(
|
|
103
|
+
{"version": 1, "hooks": {DEVIN_PROBE_EVENT: [{"hooks": [{"type": "command", "command": "true"}]}]}}),
|
|
104
|
+
encoding="utf-8")
|
|
105
|
+
env = dict(os.environ, XDG_CONFIG_HOME=temp)
|
|
106
|
+
try:
|
|
107
|
+
done = subprocess.run([binary, "doctor"], cwd=temp, env=env, timeout=timeout,
|
|
108
|
+
stdin=subprocess.DEVNULL, capture_output=True, text=True,
|
|
109
|
+
encoding="utf-8", errors="replace")
|
|
110
|
+
except subprocess.TimeoutExpired:
|
|
111
|
+
raise ProbeError("devin doctor não respondeu em " + str(int(timeout)) + " s")
|
|
112
|
+
except OSError as exc:
|
|
113
|
+
raise ProbeError("devin doctor não iniciou (" + exc.__class__.__name__ + ")")
|
|
114
|
+
found = _DEVIN_EXPECTED.search((done.stderr or "") + "\n" + (done.stdout or ""))
|
|
115
|
+
if not found:
|
|
116
|
+
raise ProbeError("devin não listou os eventos aceitos de hooks")
|
|
117
|
+
if found.group("refused") != DEVIN_PROBE_EVENT:
|
|
118
|
+
# Outro evento foi recusado: a lista pode não ser a resposta à nossa sonda.
|
|
119
|
+
raise ProbeError("devin recusou " + (found.group("refused") or "um evento") + ", não a sonda")
|
|
120
|
+
events = re.findall(r"`([^`]+)`", found.group("accepted"))
|
|
121
|
+
if not events:
|
|
122
|
+
raise ProbeError("lista de eventos do devin vazia")
|
|
123
|
+
return events
|
|
124
|
+
|
|
125
|
+
|
|
53
126
|
def camel(event):
|
|
54
127
|
return event[:1].lower() + event[1:]
|
|
55
128
|
|
package/docs/cli.md
CHANGED
|
@@ -69,6 +69,8 @@ Nome: `@andespindola/crow-skills`; `publishConfig.access` é `public` desde a 0.
|
|
|
69
69
|
|
|
70
70
|
A publicação é feita pelo workflow `.github/workflows/publish.yml` ao empurrar uma tag `vX.Y.Z` igual à `version` do `package.json` (ou por `workflow_dispatch` em `main`). Ele autentica por Trusted Publishing (OIDC), sem token npm em secret, e roda check, testes e o teste de instalação isolada antes de `npm publish`. O trusted publisher do pacote no npmjs.com deve apontar para `andersonflima/crow-skills` e `publish.yml`.
|
|
71
71
|
|
|
72
|
+
Depois do `npm publish`, o pipeline confirma a versão no próprio registry: consulta `https://registry.npmjs.org/@andespindola%2Fcrow-skills` sem npm e sem credencial e espera a versão do `package.json` aparecer em `.versions` e virar o `dist-tags.latest`, tentando a cada 20 s por até 15 minutos e registrando cada tentativa no log. Isso existe porque o log do `npm publish` sozinho não distingue uma publicação real de um `--dry-run` (que imprime um `+ pacote@versao` praticamente igual) nem de um passo pulado por gate, que deixa o job verde sem ter publicado nada. Propagação de alguns minutos até dezenas de minutos é normal e **não** é falha: passado o teto, o job termina com sucesso e emite um aviso com o comando `curl -s https://registry.npmjs.org/@andespindola%2Fcrow-skills | jq -r '.["dist-tags"].latest'` para conferir depois.
|
|
73
|
+
|
|
72
74
|
Depois de a versão estar publicada e a conta estar autenticada com acesso:
|
|
73
75
|
|
|
74
76
|
```sh
|
|
@@ -142,6 +144,8 @@ Desde a 0.8.0, a origem registrada é também uma identidade (`source_id`): `git
|
|
|
142
144
|
```sh
|
|
143
145
|
crow-skills status --agents claude,codex --global
|
|
144
146
|
crow-skills doctor --agents claude,codex --global --json
|
|
147
|
+
crow-skills skills # versão e estado de cada skill instalada
|
|
148
|
+
crow-skills skills --outdated # só o que não está na revisão da fonte
|
|
145
149
|
crow-skills uninstall crow-debug --agents claude --global --yes
|
|
146
150
|
crow-skills rollback --transaction ID_RETORNADO --dry-run
|
|
147
151
|
crow-skills rollback --transaction ID_RETORNADO --yes
|
|
@@ -161,6 +165,31 @@ crow-skills recover --yes
|
|
|
161
165
|
|
|
162
166
|
Transações protegem contra falhas comuns/interrupção de processo, não contra toda falha de hardware/filesystem. Locks coordenam processos deste CLI no mesmo perfil, não um processo hostil ou editor concorrente com as mesmas permissões do usuário. Não edite a instalação enquanto a operação estiver aplicando.
|
|
163
167
|
|
|
168
|
+
## Versões das skills (skills)
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
crow-skills skills # todas, por skill e agente
|
|
172
|
+
crow-skills skills --outdated # só o que não está em dia
|
|
173
|
+
crow-skills skills --agents claude --json # para automação
|
|
174
|
+
crow-skills skills --check-npm # também confere a última versão publicada no npm
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`skills` é somente leitura: não grava estado, não instala e não consulta a rede (a única exceção é `--check-npm`). Mostra, no cabeçalho, a versão do crow-skills, o caminho da fonte e a revisão curta (12 hex) dela; depois uma linha por skill com `skill | agentes | revisão | estado`. Uma skill instalada em mais de um agente é uma linha só, com os agentes agregados; quando os estados divergem entre agentes, cada estado vira a sua linha.
|
|
178
|
+
|
|
179
|
+
| Estado | Significado |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `latest` | instalada e igual à fonte atual deste crow-skills: mesmo conteúdo, mesma revisão e mesma origem registrada. É o único estado em dia. |
|
|
182
|
+
| `desatualizada` | instalada e íntegra, mas a revisão ou a origem registrada não é a da fonte atual: falta rodar `update`. |
|
|
183
|
+
| `modificada` | editada no disco; `update` só a substitui com `--overwrite-modified`. |
|
|
184
|
+
| `desativada` | instalada e desabilitada (por exemplo, pelo Uniflow). |
|
|
185
|
+
| `ausente` | registrada no estado e não encontrada no disco. |
|
|
186
|
+
| `nao_gerenciada` | o que está ali não é uma instalação gerenciada por este CLI (pacote de terceiros, link do Uniflow, destino vazio). |
|
|
187
|
+
| `indeterminada` | não foi possível ler o destino com segurança (`unsafe`). |
|
|
188
|
+
|
|
189
|
+
O rodapé só aparece quando há problema: quantas desatualizadas, modificadas e ausentes, e o comando exato que conserta, `crow-skills update --all --agents <agentes afetados> --global --yes` (com uma linha a mais lembrando que skill editada ou ausente só é substituída acrescentando `--overwrite-modified`). Com `--check-npm`, uma consulta a `npm view @andespindola/crow-skills version` diz se o CLI instalado é a última versão publicada; sem npm no PATH, sem rede ou com erro, o comando informa que não deu para conferir e nunca inventa um número.
|
|
190
|
+
|
|
191
|
+
`status` e `doctor` usam a mesma classificação e passaram a avisar quando há skill instalada atrasada em relação à fonte: aviso `skills_outdated` (campos `skills`, `agents`, `message`, `fix`) no `--json` e a mesma linha com `Para corrigir:` na saída humana. O `doctor` fica `healthy:false` nesse estado — uma skill velha no disco não é mais `✓ healthy` — e volta a `healthy:true` depois do `update`.
|
|
192
|
+
|
|
164
193
|
## Automação por agente
|
|
165
194
|
|
|
166
195
|
```sh
|
|
@@ -348,9 +377,42 @@ Aplicadas por padrão junto com `install`/`update` em `--global` para os agentes
|
|
|
348
377
|
| gemini-command | gemini (Gemini CLI) | `~/.gemini/commands/crow.toml` (arquivo próprio) | `/crow on\|off\|status` por `!{…}` (o Gemini pede confirmação do shell, salvo política que o permita) |
|
|
349
378
|
| agy-hooks | gemini (Antigravity) | chave `crow-skills` em `~/.gemini/config/hooks.json` | modo por prompt (`PreInvocation` → `injectSteps`), `/crow …` interceptado quando o agy envia o texto (`lastUserInput`, 1.4.7+), eventos (`PreToolUse`/`PostToolUse` agrupados com `matcher`, como o agy exige) |
|
|
350
379
|
| agy-statusline | gemini (Antigravity) | `statusLine` em `~/.gemini/antigravity-cli/settings.json` com `stack_with_default: true` | indicador e lista de processos empilhados sob a linha padrão do agy |
|
|
351
|
-
| devin-hooks | devin | `hooks` em `~/.config/devin/config.json` | modo por prompt, `/crow …` interceptado, `systemMessage` quando muda, eventos |
|
|
380
|
+
| devin-hooks | devin | `hooks` em `~/.config/devin/config.json` | modo por prompt, `/crow …` interceptado, `systemMessage` quando muda, eventos — só os que a build instalada aceita (veja "Eventos do devin") |
|
|
352
381
|
| tmux-status | claude, codex, gemini (agy), devin | `~/.config/crow-skills/tmux.conf` (arquivo próprio) e um bloco `source-file -q` no fim do seu `tmux.conf` | `▸▸ crow on · crow ▸ …` acrescentado ao `status-right` (`set -ag`, sem substituir o seu), com o diretório do painel ativo |
|
|
353
382
|
|
|
383
|
+
### Eventos do devin
|
|
384
|
+
|
|
385
|
+
O devin valida a chave `hooks` contra o enum de eventos da build instalada e, ao recusar **um**
|
|
386
|
+
evento, descarta o **bloco inteiro** ("Using the default ({}) for this setting"). Um evento que a
|
|
387
|
+
sua build não conhece não degradaria um hook: desligaria todos, com o arquivo parecendo instalado.
|
|
388
|
+
|
|
389
|
+
Por isso a lista não é presumida, é perguntada. Antes de gravar, o crow-skills põe um evento
|
|
390
|
+
propositalmente inválido num `config.json` descartável, aponta `XDG_CONFIG_HOME` para ele e roda
|
|
391
|
+
`devin doctor` (medido: 0,56 s, teto de 8 s): ao recusar, o devin nomeia todos os eventos que
|
|
392
|
+
conhece. O seu `~/.config/devin/config.json` não é lido nem escrito nessa consulta, e só os eventos
|
|
393
|
+
aceitos são gravados — os omitidos aparecem na nota da integração.
|
|
394
|
+
|
|
395
|
+
A leitura dessa resposta é deliberadamente exigente, porque o devin usa a mesma frase para todo
|
|
396
|
+
enum que recusa (`theme_mode`, `ProxyMode`) e os outros avisos saem antes. Só vale a mensagem que
|
|
397
|
+
fala da chave `hooks`, que recusa exatamente o evento da sonda e cuja lista termina no
|
|
398
|
+
`. Using the default` do próprio devin. Qualquer coisa fora disso — lista quebrada em duas linhas,
|
|
399
|
+
texto colorido, mensagem traduzida — **não casa**, e não casar é tratado como "não deu para
|
|
400
|
+
perguntar". Uma lista lida pela metade seria pior que falha nenhuma: gravaria quatro eventos como
|
|
401
|
+
se fossem oito e os outros hooks desapareceriam em silêncio.
|
|
402
|
+
|
|
403
|
+
Quando o devin não pode ser consultado (ausente do PATH, sem responder, binário trocado no meio da
|
|
404
|
+
consulta), o crow-skills grava apenas os eventos que toda build conhecida aceita e diz isso, em vez
|
|
405
|
+
de adivinhar. Se a resposta do devin for válida mas não tiver **nenhum** evento em comum com os que
|
|
406
|
+
o crow-skills usa, nada é gravado e a integração é recusada com essa explicação — cair no conjunto
|
|
407
|
+
conservador ali seria gravar justamente o que aquela build recusa.
|
|
408
|
+
|
|
409
|
+
`crow-skills doctor` reconsulta e reporta `devin-hooks` como desabilitado, em vez de ✓, quando o
|
|
410
|
+
arquivo tem evento que a build atual recusa (um downgrade do devin, por exemplo) ou quando a chave
|
|
411
|
+
`hooks` não é um objeto JSON — o devin descarta o bloco nos dois casos. Arquivo ilegível ou com
|
|
412
|
+
hard link vira "não verificado" com o motivo, sem derrubar o resto do relatório. O `status`, que não
|
|
413
|
+
consulta o devin, responde verde offline quando tudo o que está escrito pertence ao conjunto
|
|
414
|
+
conservador, e só pede o `doctor` quando há evento fora dele.
|
|
415
|
+
|
|
354
416
|
Regras de preservação:
|
|
355
417
|
|
|
356
418
|
- Antes de editar, o arquivo original vai para `~/.local/state/crow-skills/integrations/backups/` (privado). Gravação atômica, nunca através de symlink ou hard link; prompts compartilhados e `~/.config/uniflow` são recusados como no guia.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andespindola/crow-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Versioned, complementary agent skills with a protected multi-agent installer",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"LICENSE",
|
|
15
|
+
"THIRD-PARTY-NOTICES.md",
|
|
15
16
|
"bin/npm-crow-skills.cjs",
|
|
16
17
|
"crow_skills/*.py",
|
|
17
18
|
"catalog.json",
|