@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.
Files changed (85) hide show
  1. package/LICENSE +32 -4
  2. package/THIRD-PARTY-NOTICES.md +43 -0
  3. package/catalog.json +148 -38
  4. package/crow_skills/__init__.py +1 -1
  5. package/crow_skills/autoupdate.py +6 -1
  6. package/crow_skills/cli.py +189 -1
  7. package/crow_skills/guide.py +3 -0
  8. package/crow_skills/integrations.py +105 -1
  9. package/crow_skills/storage.py +4 -0
  10. package/crow_skills/trust.py +73 -0
  11. package/docs/cli.md +63 -1
  12. package/package.json +2 -1
  13. package/release.json +202 -57
  14. package/skills/crow-accessibility/SKILL.md +7 -5
  15. package/skills/crow-accessibility/references/verificacao.md +102 -0
  16. package/skills/crow-agent-security/SKILL.md +8 -4
  17. package/skills/crow-agent-security/references/limites-agente.md +17 -0
  18. package/skills/crow-api-test/SKILL.md +2 -0
  19. package/skills/crow-api-test/references/casos-negativos.md +16 -0
  20. package/skills/crow-architecture/SKILL.md +2 -0
  21. package/skills/crow-architecture/references/modulos-profundos.md +18 -0
  22. package/skills/crow-backend/SKILL.md +3 -1
  23. package/skills/crow-backend/references/servico-backend.md +13 -0
  24. package/skills/crow-be-human/SKILL.md +2 -1
  25. package/skills/crow-be-human/references/voz-e-registro.md +35 -0
  26. package/skills/crow-behaviour/SKILL.md +21 -7
  27. package/skills/crow-behaviour/references/briefing-executavel.md +65 -0
  28. package/skills/crow-behaviour/references/ciclo-de-revisao.md +104 -0
  29. package/skills/crow-behaviour/references/micro-agents.md +52 -5
  30. package/skills/crow-behaviour/references/progress-ledger.md +3 -1
  31. package/skills/crow-behaviour/scripts/crow_micro.py +45 -0
  32. package/skills/crow-behaviour/scripts/micro_agent_instructions.md +15 -3
  33. package/skills/crow-browser/SKILL.md +45 -3
  34. package/skills/crow-browser/references/mecanismo-de-browser.md +24 -0
  35. package/skills/crow-browser/references/pagina.md +43 -0
  36. package/skills/crow-debug/SKILL.md +1 -1
  37. package/skills/crow-design/SKILL.md +6 -6
  38. package/skills/crow-design/references/contrato.md +84 -0
  39. package/skills/crow-discovery/SKILL.md +5 -4
  40. package/skills/crow-discovery/references/pesquisa-sem-vies.md +33 -0
  41. package/skills/crow-documents/SKILL.md +9 -3
  42. package/skills/crow-documents/references/ooxml.md +43 -0
  43. package/skills/crow-documents/references/pdf.md +31 -0
  44. package/skills/crow-documents/references/planilha.md +35 -0
  45. package/skills/crow-domain/SKILL.md +2 -0
  46. package/skills/crow-domain/references/invariante-e-identidade.md +11 -0
  47. package/skills/crow-frontend/SKILL.md +6 -5
  48. package/skills/crow-growth/SKILL.md +2 -1
  49. package/skills/crow-growth/references/experimento-e-metrica.md +39 -0
  50. package/skills/crow-infra/SKILL.md +6 -5
  51. package/skills/crow-infra/references/kubernetes.md +6 -0
  52. package/skills/crow-interface/SKILL.md +1 -1
  53. package/skills/crow-interface/reference/adapt.md +8 -0
  54. package/skills/crow-interface/reference/layout.md +4 -1
  55. package/skills/crow-interface/reference/workflow.md +2 -0
  56. package/skills/crow-media/SKILL.md +5 -5
  57. package/skills/crow-media/references/producao.md +63 -0
  58. package/skills/crow-native-mobile/SKILL.md +1 -1
  59. package/skills/crow-observe/SKILL.md +8 -4
  60. package/skills/crow-observe/references/cardinalidade.md +11 -0
  61. package/skills/crow-performance/SKILL.md +5 -5
  62. package/skills/crow-performance/references/measurement.md +6 -0
  63. package/skills/crow-platform/SKILL.md +4 -4
  64. package/skills/crow-postgres/SKILL.md +3 -0
  65. package/skills/crow-postgres/references/migracao-online.md +17 -0
  66. package/skills/crow-product-leadership/SKILL.md +4 -3
  67. package/skills/crow-product-leadership/references/decisao-e-pessoas.md +35 -0
  68. package/skills/crow-qa/SKILL.md +1 -1
  69. package/skills/crow-release/SKILL.md +10 -5
  70. package/skills/crow-release/references/transicao.md +13 -0
  71. package/skills/crow-reverse/SKILL.md +1 -1
  72. package/skills/crow-secrets/SKILL.md +2 -2
  73. package/skills/crow-security-review/SKILL.md +12 -5
  74. package/skills/crow-security-scan/SKILL.md +8 -3
  75. package/skills/crow-security-scan/references/evidencia-scan.md +17 -0
  76. package/skills/crow-security-test/SKILL.md +5 -1
  77. package/skills/crow-security-test/references/prova-dinamica.md +24 -0
  78. package/skills/crow-seo/SKILL.md +6 -4
  79. package/skills/crow-seo/references/saida-tecnica.md +78 -0
  80. package/skills/crow-skill-author/SKILL.md +10 -6
  81. package/skills/crow-skill-author/references/medicao.md +85 -0
  82. package/skills/crow-spec/SKILL.md +4 -3
  83. package/skills/crow-spec/references/criterio-e-fatia.md +27 -0
  84. package/skills/crow-threat-model/SKILL.md +5 -1
  85. package/skills/crow-threat-model/references/ciclo-ameaca.md +15 -0
@@ -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
- result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe", "external_link") for r in rows + guides)
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.")
@@ -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 DEVIN_EVENTS}
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 = {}
@@ -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
@@ -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.23.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",