@andespindola/crow-skills 0.1.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 (121) hide show
  1. package/README.md +62 -0
  2. package/bin/npm-crow-skills.cjs +20 -0
  3. package/catalog.json +398 -0
  4. package/crow_skills/__init__.py +3 -0
  5. package/crow_skills/__main__.py +3 -0
  6. package/crow_skills/agents.py +45 -0
  7. package/crow_skills/cli.py +241 -0
  8. package/crow_skills/npm_entry.py +14 -0
  9. package/crow_skills/source.py +256 -0
  10. package/crow_skills/storage.py +396 -0
  11. package/docs/cli-security.md +33 -0
  12. package/docs/cli.md +138 -0
  13. package/package.json +35 -0
  14. package/release.json +607 -0
  15. package/skills/crow-accessibility/SKILL.md +33 -0
  16. package/skills/crow-agent-security/SKILL.md +33 -0
  17. package/skills/crow-api-test/SKILL.md +34 -0
  18. package/skills/crow-architecture/SKILL.md +33 -0
  19. package/skills/crow-backend/SKILL.md +33 -0
  20. package/skills/crow-behaviour/SKILL.md +70 -0
  21. package/skills/crow-behaviour/agents/openai.yaml +4 -0
  22. package/skills/crow-behaviour/references/orchestration.md +64 -0
  23. package/skills/crow-behaviour/references/quality-patterns.md +56 -0
  24. package/skills/crow-browser/SKILL.md +33 -0
  25. package/skills/crow-debug/SKILL.md +33 -0
  26. package/skills/crow-design/SKILL.md +33 -0
  27. package/skills/crow-discovery/SKILL.md +33 -0
  28. package/skills/crow-documents/SKILL.md +33 -0
  29. package/skills/crow-domain/SKILL.md +33 -0
  30. package/skills/crow-frontend/SKILL.md +33 -0
  31. package/skills/crow-growth/SKILL.md +33 -0
  32. package/skills/crow-infra/SKILL.md +38 -0
  33. package/skills/crow-infra/references/kubernetes.md +3 -0
  34. package/skills/crow-interface/NOTICE.md +20 -0
  35. package/skills/crow-interface/SKILL.md +55 -0
  36. package/skills/crow-interface/agents/impeccable_asset_producer.toml +43 -0
  37. package/skills/crow-interface/agents/impeccable_documenter.toml +28 -0
  38. package/skills/crow-interface/agents/impeccable_finish_reviewer.toml +42 -0
  39. package/skills/crow-interface/agents/impeccable_manual_edit_applier.toml +96 -0
  40. package/skills/crow-interface/agents/openai.yaml +3 -0
  41. package/skills/crow-interface/licenses/Apache-2.0.txt +191 -0
  42. package/skills/crow-interface/licenses/Impeccable-NOTICE.md +11 -0
  43. package/skills/crow-interface/licenses/modern-screenshot-MIT.txt +9 -0
  44. package/skills/crow-interface/licenses/platform-design-MIT.txt +21 -0
  45. package/skills/crow-interface/reference/adapt.md +320 -0
  46. package/skills/crow-interface/reference/adapt.native.md +60 -0
  47. package/skills/crow-interface/reference/android.md +48 -0
  48. package/skills/crow-interface/reference/animate.md +91 -0
  49. package/skills/crow-interface/reference/audit.md +139 -0
  50. package/skills/crow-interface/reference/audit.native.md +141 -0
  51. package/skills/crow-interface/reference/bolder.md +35 -0
  52. package/skills/crow-interface/reference/clarify.md +96 -0
  53. package/skills/crow-interface/reference/colorize.md +88 -0
  54. package/skills/crow-interface/reference/craft-floor.md +52 -0
  55. package/skills/crow-interface/reference/craft.md +7 -0
  56. package/skills/crow-interface/reference/critique.md +828 -0
  57. package/skills/crow-interface/reference/degraded/asset-producer.md +5 -0
  58. package/skills/crow-interface/reference/degraded/documenter.md +5 -0
  59. package/skills/crow-interface/reference/degraded/finish-reviewer.md +5 -0
  60. package/skills/crow-interface/reference/degraded/manual-edit-applier.md +5 -0
  61. package/skills/crow-interface/reference/delight.md +72 -0
  62. package/skills/crow-interface/reference/distill.md +113 -0
  63. package/skills/crow-interface/reference/doctor.md +56 -0
  64. package/skills/crow-interface/reference/document.md +418 -0
  65. package/skills/crow-interface/reference/extract.md +70 -0
  66. package/skills/crow-interface/reference/generate.md +103 -0
  67. package/skills/crow-interface/reference/harden.md +347 -0
  68. package/skills/crow-interface/reference/hooks.md +113 -0
  69. package/skills/crow-interface/reference/init.md +133 -0
  70. package/skills/crow-interface/reference/ios.md +53 -0
  71. package/skills/crow-interface/reference/layout.md +90 -0
  72. package/skills/crow-interface/reference/live-setup.md +106 -0
  73. package/skills/crow-interface/reference/live.md +329 -0
  74. package/skills/crow-interface/reference/new-work.md +149 -0
  75. package/skills/crow-interface/reference/onboard.md +236 -0
  76. package/skills/crow-interface/reference/operate.md +63 -0
  77. package/skills/crow-interface/reference/optimize.md +260 -0
  78. package/skills/crow-interface/reference/overdrive.md +129 -0
  79. package/skills/crow-interface/reference/polish.md +107 -0
  80. package/skills/crow-interface/reference/quieter.md +101 -0
  81. package/skills/crow-interface/reference/roles/asset-producer.md +38 -0
  82. package/skills/crow-interface/reference/roles/documenter.md +23 -0
  83. package/skills/crow-interface/reference/roles/finish-reviewer.md +37 -0
  84. package/skills/crow-interface/reference/roles/manual-edit-applier.md +91 -0
  85. package/skills/crow-interface/reference/routing.md +26 -0
  86. package/skills/crow-interface/reference/runtime.md +30 -0
  87. package/skills/crow-interface/reference/shape.md +61 -0
  88. package/skills/crow-interface/reference/typeset.md +86 -0
  89. package/skills/crow-interface/reference/visualize.md +48 -0
  90. package/skills/crow-interface/reference/workflow.md +53 -0
  91. package/skills/crow-interface/scripts/VERSION +1 -0
  92. package/skills/crow-interface/scripts/command-metadata.json +98 -0
  93. package/skills/crow-interface/scripts/data/font-index-failures.json +121 -0
  94. package/skills/crow-interface/scripts/data/font-index.json +1 -0
  95. package/skills/crow-interface/scripts/engine-manifest.json +13 -0
  96. package/skills/crow-interface/scripts/impeccable +106 -0
  97. package/skills/crow-interface/scripts/impeccable.cmd +3 -0
  98. package/skills/crow-interface/scripts/live-browser-dom.js +167 -0
  99. package/skills/crow-interface/scripts/live-browser-ignores.js +242 -0
  100. package/skills/crow-interface/scripts/live-browser-session.js +144 -0
  101. package/skills/crow-interface/scripts/live-browser.js +13510 -0
  102. package/skills/crow-interface/scripts/modern-screenshot.umd.js +14 -0
  103. package/skills/crow-media/SKILL.md +33 -0
  104. package/skills/crow-native-mobile/SKILL.md +33 -0
  105. package/skills/crow-observe/SKILL.md +33 -0
  106. package/skills/crow-performance/SKILL.md +38 -0
  107. package/skills/crow-performance/references/measurement.md +3 -0
  108. package/skills/crow-platform/SKILL.md +33 -0
  109. package/skills/crow-postgres/SKILL.md +33 -0
  110. package/skills/crow-product-leadership/SKILL.md +33 -0
  111. package/skills/crow-qa/SKILL.md +34 -0
  112. package/skills/crow-release/SKILL.md +35 -0
  113. package/skills/crow-reverse/SKILL.md +33 -0
  114. package/skills/crow-secrets/SKILL.md +33 -0
  115. package/skills/crow-security-review/SKILL.md +33 -0
  116. package/skills/crow-security-scan/SKILL.md +33 -0
  117. package/skills/crow-security-test/SKILL.md +33 -0
  118. package/skills/crow-seo/SKILL.md +33 -0
  119. package/skills/crow-skill-author/SKILL.md +34 -0
  120. package/skills/crow-spec/SKILL.md +33 -0
  121. package/skills/crow-threat-model/SKILL.md +33 -0
@@ -0,0 +1,241 @@
1
+ """Human and agent CLI. Noninteractive mutations require explicit --yes."""
2
+ import argparse
3
+ import json
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .agents import AGENTS, destinations, inventory
9
+ from .source import CrowError, Source, valid_name
10
+ from .storage import Store, checked
11
+
12
+ DEFAULT_SOURCE = Path(__file__).resolve().parents[1]
13
+ MUTATIONS = {"install", "update", "uninstall", "rollback", "recover"}
14
+
15
+
16
+ class Parser(argparse.ArgumentParser):
17
+ def error(self, message):
18
+ raise CrowError("usage", message)
19
+
20
+
21
+ def parser():
22
+ common = Parser(add_help=False)
23
+ for flag, kwargs in [
24
+ ("--source", {"help": "Clone Git existente (padrão: checkout do CLI)"}),
25
+ ("--ref", {"help": "Commit/tag/ref da fonte; padrão HEAD, sem checkout"}),
26
+ ("--home", {"help": "Perfil local alternativo, para ambientes isolados"}),
27
+ ("--agents", {"help": "Agentes separados por vírgula: " + ",".join(AGENTS)}),
28
+ ("--global", {"dest": "global_scope", "action": "store_true", "help": "Escopo do usuário"}),
29
+ ("--project", {"help": "Diretório físico do projeto"}),
30
+ ("--dest", {"help": "Diretório de skills explícito, para outro host"}),
31
+ ("--json", {"action": "store_true", "help": "Envelope JSON; nunca abre prompts"}),
32
+ ("--non-interactive", {"action": "store_true"}),
33
+ ("--yes", {"action": "store_true", "help": "Aplicar o escopo explícito sem prompt"}),
34
+ ("--dry-run", {"action": "store_true", "help": "Preview sem gravar estado ou instalar"}),
35
+ ("--fetch", {"action": "store_true", "help": "Buscar origin explicitamente; não cria clone"}),
36
+ ]:
37
+ common.add_argument(flag, default=argparse.SUPPRESS, **kwargs)
38
+ p = Parser(prog="crow-skills", parents=[common], description="Instale pacotes crow completos, por agente e revisão, sem executar scripts de skills.")
39
+ p.add_argument("--version", action="store_true")
40
+ sub = p.add_subparsers(dest="command")
41
+ for name in ("list", "agents", "status", "doctor"):
42
+ sub.add_parser(name, parents=[common])
43
+ for name in ("info", "related"):
44
+ child = sub.add_parser(name, parents=[common])
45
+ child.add_argument("name")
46
+ for name in ("install", "update", "uninstall"):
47
+ child = sub.add_parser(name, parents=[common])
48
+ child.add_argument("skills", nargs="*")
49
+ child.add_argument("--all", action="store_true", help="Todos do catálogo (install) ou gerenciados (update/uninstall)")
50
+ if name != "uninstall":
51
+ child.add_argument("--replace", action="store_true", help="Autorizar substituição com backup de pacote não gerenciado")
52
+ child.add_argument("--with-handoffs", action="store_true", help="Incluir apenas handoffs diretos opcionais do catálogo")
53
+ child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição/remoção de edições locais")
54
+ for name in ("rollback", "recover"):
55
+ child = sub.add_parser(name, parents=[common])
56
+ child.add_argument("--transaction", required=name == "rollback")
57
+ return p
58
+
59
+
60
+ def prompt(text):
61
+ print(text, file=sys.stderr, end=" ", flush=True)
62
+ line = sys.stdin.readline()
63
+ if not line:
64
+ raise CrowError("cancelled", "Entrada encerrada; nenhuma instalação autorizada.")
65
+ return line.strip()
66
+
67
+
68
+ def terminal(text):
69
+ """Do not render terminal escapes or invisible bidi controls from metadata."""
70
+ return "".join(c if c.isprintable() or c in "\n\t" else c.encode("unicode_escape").decode("ascii") for c in str(text))
71
+
72
+
73
+ def scope(args, home, interactive=False, read=False):
74
+ if sum(bool(getattr(args, k, None)) for k in ("global_scope", "project", "dest")) > 1:
75
+ raise CrowError("usage", "Escolha somente um de --global, --project ou --dest.")
76
+ agents = getattr(args, "agents", None)
77
+ project = getattr(args, "project", None)
78
+ custom = getattr(args, "dest", None)
79
+ if not read and not any(getattr(args, k, None) for k in ("global_scope", "project", "dest")):
80
+ if not interactive:
81
+ raise CrowError("usage", "Mutações exigem escopo explícito: --global, --project PATH ou --dest PATH.")
82
+ selected = prompt("Escopo: [p]rojeto atual ou [g]lobal? [p]").lower()
83
+ if selected not in ("", "p", "g"):
84
+ raise CrowError("usage", "Escopo inválido.")
85
+ if selected != "g":
86
+ project = str(Path.cwd())
87
+ if project:
88
+ project = checked(project)
89
+ if not project.is_dir():
90
+ raise CrowError("usage", "O diretório do projeto deve existir.")
91
+ if not agents and not custom:
92
+ if read:
93
+ agents = ",".join(AGENTS)
94
+ elif interactive:
95
+ agents = prompt("Agentes separados por vírgula (" + ",".join(AGENTS) + "):")
96
+ return destinations(agents, home, project, custom)
97
+
98
+
99
+ def authorize(args, interactive, summary):
100
+ if getattr(args, "dry_run", False) or getattr(args, "yes", False):
101
+ return
102
+ if not interactive:
103
+ raise CrowError("confirmation_required", "Use --dry-run para preview ou --yes para aplicar o escopo explícito.")
104
+ print(json.dumps(summary, ensure_ascii=False, indent=2), file=sys.stderr)
105
+ if prompt("Aplicar estas alterações? [s/N]").lower() not in ("s", "sim", "y", "yes"):
106
+ raise CrowError("cancelled", "Operação cancelada; nenhuma instalação alterada.")
107
+
108
+
109
+ def relationship_view(source, name, store, targets):
110
+ relations = source.related(name)
111
+ names = set(relations["handoffs"] + relations["used_by"] + relations["behaviour_overlays"] + relations["can_coordinate"])
112
+ return {**relations, "availability": [r for r in store.status(targets, names) if r["name"] in names] if names else [],
113
+ "meaning": "Handoffs e overlays são complementos opcionais, não dependências nem ordem automática de execução."}
114
+
115
+
116
+ def run(args):
117
+ if args.version:
118
+ return {"version": __version__}
119
+ if not args.command:
120
+ raise CrowError("usage", "Escolha um comando; use crow-skills --help.")
121
+ home = checked(getattr(args, "home", Path.home()))
122
+ store = Store(home)
123
+ interactive = sys.stdin.isatty() and not getattr(args, "non_interactive", False) and not getattr(args, "json", False)
124
+ if getattr(args, "fetch", False) and (getattr(args, "dry_run", False) or args.command not in ("install", "update", "list", "info", "related")):
125
+ raise CrowError("usage", "--fetch requer um comando com fonte e não pode ser combinado com --dry-run.")
126
+ if args.command == "agents":
127
+ return {"agents": inventory(home), "note": "Detecção por PATH/diretórios não comprova carregamento da skill."}
128
+ if args.command in ("status", "doctor"):
129
+ targets = scope(args, home, read=True)
130
+ rows = store.status(targets)
131
+ pending = [t["id"] for t in store.pending()]
132
+ result = {"installations": rows, "pending_transactions": pending,
133
+ "activation_verified": False, "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
134
+ if args.command == "doctor":
135
+ result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows), agents=inventory(home))
136
+ return result
137
+ if args.command in ("rollback", "recover"):
138
+ if any(getattr(args, k, None) for k in ("agents", "dest", "project", "global_scope")):
139
+ raise CrowError("usage", "Rollback/recover usam os destinos registrados na transação, sem filtros de agente/escopo.")
140
+ ids = [args.transaction] if args.transaction else [t["id"] for t in store.pending()]
141
+ transactions = [store.journal(txid) for txid in ids]
142
+ expected = ("prepared", "restoring") if args.command == "recover" else ("committed",)
143
+ if any(t["phase"] not in expected for t in transactions):
144
+ raise CrowError("invalid_transaction", "Transação incompatível com a operação.")
145
+ summary = {"transactions": [{"id": t["id"], "targets": list(t["before"])} for t in transactions]}
146
+ if transactions:
147
+ authorize(args, interactive, summary)
148
+ if not getattr(args, "dry_run", False):
149
+ for txid in ids:
150
+ store.rollback(txid, recovery=args.command == "recover")
151
+ return {**summary, "dry_run": getattr(args, "dry_run", False), "applied": not getattr(args, "dry_run", False)}
152
+ source = None
153
+ if args.command != "uninstall":
154
+ source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"), getattr(args, "fetch", False))
155
+ if args.command == "list":
156
+ return {"revision": source.revision, "source": str(source.path), "skills": list(source.catalog.values()),
157
+ "composition": {"overlays": source.overlays}, "handoffs_are_optional": True}
158
+ if args.command in ("info", "related"):
159
+ source.require(args.name)
160
+ targets = scope(args, home, read=True)
161
+ return {"revision": source.revision, "skill": source.catalog[args.name],
162
+ "relationships": relationship_view(source, args.name, store, targets)}
163
+ targets = scope(args, home, interactive)
164
+ names = args.skills
165
+ if names and args.all:
166
+ raise CrowError("usage", "Use nomes ou --all, não ambos.")
167
+ if args.command == "install" and not names and not args.all:
168
+ if not interactive:
169
+ raise CrowError("usage", "Escolha nomes de skills ou --all.")
170
+ for name, item in source.catalog.items():
171
+ print(terminal(name + " — " + item.get("title", name)), file=sys.stderr)
172
+ names = prompt("Skills separadas por vírgula:").split(",")
173
+ names = [n.strip() for n in names if n.strip()]
174
+ if not names:
175
+ raise CrowError("usage", "Nenhuma skill selecionada.")
176
+ if args.command == "uninstall" and not names and not args.all:
177
+ raise CrowError("usage", "Uninstall exige nomes explícitos ou --all.")
178
+ if args.command == "install" and args.all:
179
+ names = list(source.catalog)
180
+ names = list(dict.fromkeys(names))
181
+ if getattr(args, "with_handoffs", False):
182
+ if not names:
183
+ raise CrowError("usage", "--with-handoffs exige nomes explícitos.")
184
+ for name in list(names):
185
+ source.require(name)
186
+ names.extend(n for n in source.catalog[name]["next"] if n not in names)
187
+ plan = store.plan(args.command, names, targets, source, getattr(args, "replace", False), args.overwrite_modified)
188
+ summary = {"changes": plan["changes"], "revision": source.revision if source else None,
189
+ "relationships": {n: source.related(n) for n in names} if source else {},
190
+ "handoffs_are_optional": True, "activation_verified": False}
191
+ if plan["items"]:
192
+ authorize(args, interactive, summary)
193
+ txid = None if getattr(args, "dry_run", False) else store.apply(plan)
194
+ return {**summary, "transaction": txid, "dry_run": getattr(args, "dry_run", False),
195
+ "applied": not getattr(args, "dry_run", False),
196
+ "reload_hint": ("Somente preview; nada instalado." if getattr(args, "dry_run", False) else
197
+ "Operação de arquivos concluída; carregamento/ativação devem ser conferidos no host.")}
198
+
199
+
200
+ def emit(command, data, machine):
201
+ envelope = {"schema_version": 1, "ok": True, "command": command, "data": data}
202
+ if machine:
203
+ print(json.dumps(envelope, ensure_ascii=False, separators=(",", ":")))
204
+ elif command == "list":
205
+ print("Fonte: " + data["revision"])
206
+ for item in data["skills"]:
207
+ print(terminal(f"{item['name']} [{item['role']}] → {item['owns']}\n {item['description']}"))
208
+ else:
209
+ print(terminal(json.dumps(data, ensure_ascii=False, indent=2)))
210
+
211
+
212
+ def main(argv=None):
213
+ argv = list(sys.argv[1:] if argv is None else argv)
214
+ machine = "--json" in argv
215
+ command = None
216
+ try:
217
+ args = parser().parse_args(argv)
218
+ command = args.command or ("version" if args.version else None)
219
+ result = run(args)
220
+ emit(command, result, machine)
221
+ return 0
222
+ except CrowError as exc:
223
+ if machine:
224
+ print(json.dumps({"schema_version": 1, "ok": False, "command": command,
225
+ "error": {"code": exc.code, "message": str(exc)}}, ensure_ascii=False))
226
+ else:
227
+ print(f"Erro [{exc.code}]: {exc}", file=sys.stderr)
228
+ return 2 if exc.code == "usage" else 1
229
+ except (OSError, ValueError, KeyError, TypeError) as exc:
230
+ # No traceback or subprocess stderr in machine output; credentials stay external.
231
+ if machine:
232
+ print(json.dumps({"schema_version": 1, "ok": False, "command": command,
233
+ "error": {"code": "io_error", "message": "Falha de leitura/gravação ou estado inválido; nenhuma conclusão de sucesso."}}))
234
+ else:
235
+ print("Falha de leitura/gravação ou estado inválido. Confira permissões, origem e transações pendentes.", file=sys.stderr)
236
+ return 1
237
+ except KeyboardInterrupt:
238
+ if machine:
239
+ print(json.dumps({"schema_version": 1, "ok": False, "command": command,
240
+ "error": {"code": "interrupted", "message": "Operação interrompida; confira status/recover."}}))
241
+ return 130
@@ -0,0 +1,14 @@
1
+ """Isolated Python entry used by the npm launcher, not imported as package code."""
2
+ from pathlib import Path
3
+ import json
4
+ import sys
5
+
6
+ if sys.version_info < (3, 10):
7
+ error = {"schema_version": 1, "ok": False, "command": None,
8
+ "error": {"code": "python_version", "message": "Python 3.10+ é necessário."}}
9
+ print(json.dumps(error) if "--json" in sys.argv else error["error"]["message"])
10
+ raise SystemExit(1)
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12
+ from crow_skills.cli import main
13
+
14
+ raise SystemExit(main())
@@ -0,0 +1,256 @@
1
+ """Read committed Git objects as data. Never check out or execute package code."""
2
+ from dataclasses import dataclass
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path, PurePosixPath
6
+ import re
7
+ import stat
8
+ import subprocess
9
+
10
+ NAME = re.compile(r"crow-[a-z0-9]+(?:-[a-z0-9]+)*\Z")
11
+ SHA = re.compile(r"[a-f0-9]{40,64}\Z")
12
+ MAX_FILE = 8 * 1024 * 1024
13
+ MAX_PACKAGE = 32 * 1024 * 1024
14
+ MAX_FILES = 2000
15
+
16
+
17
+ class CrowError(Exception):
18
+ def __init__(self, code, message):
19
+ super().__init__(message)
20
+ self.code = code
21
+
22
+
23
+ def valid_name(name):
24
+ if not isinstance(name, str) or not NAME.fullmatch(name) or len(name) >= 64:
25
+ raise CrowError("invalid_name", "Nome de skill inválido; use crow-nome.")
26
+ return name
27
+
28
+
29
+ def safe_relative(value):
30
+ p = PurePosixPath(value)
31
+ if (not value or p.is_absolute() or str(p) != value or
32
+ any(x in (".", "..") for x in p.parts) or "\\" in value or
33
+ any(ord(c) < 32 or ord(c) == 127 for c in value)):
34
+ raise CrowError("unsafe_path", "Caminho de recurso inválido no pacote.")
35
+ return p
36
+
37
+
38
+ def file_entry(data, executable=False):
39
+ return {"kind": "file", "sha256": hashlib.sha256(data).hexdigest(),
40
+ "executable": bool(executable)}
41
+
42
+
43
+ def entries_for(files):
44
+ entries = {}
45
+ for name, (data, executable) in files.items():
46
+ entries[name] = file_entry(data, executable)
47
+ for parent in PurePosixPath(name).parents:
48
+ if str(parent) != ".":
49
+ entries[str(parent)] = {"kind": "dir"}
50
+ return dict(sorted(entries.items()))
51
+
52
+
53
+ def digest(entries):
54
+ return hashlib.sha256(json.dumps(entries, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
55
+
56
+
57
+ @dataclass
58
+ class Package:
59
+ name: str
60
+ files: dict
61
+ entries: dict
62
+
63
+ def materialize(self, target):
64
+ target.mkdir(mode=0o700)
65
+ for name, (data, executable) in self.files.items():
66
+ p = target / name
67
+ p.parent.mkdir(parents=True, exist_ok=True)
68
+ p.write_bytes(data)
69
+ p.chmod(0o755 if executable else 0o644)
70
+
71
+
72
+ class Source:
73
+ def __init__(self, path, ref="HEAD", fetch=False):
74
+ self.path = Path(path).expanduser().resolve(strict=True)
75
+ self.bundle = None
76
+ if (self.path / "release.json").exists() and not (self.path / ".git").exists():
77
+ if fetch:
78
+ raise CrowError("immutable_release", "Pacote npm é um snapshot; atualize o pacote npm ou selecione --source com clone Git.")
79
+ self._init_bundle()
80
+ if ref not in ("HEAD", self.revision):
81
+ raise CrowError("immutable_release", "A referência não está neste snapshot npm; selecione um clone Git para outras refs.")
82
+ raw = self.bundle_file("catalog.json")
83
+ else:
84
+ if fetch:
85
+ self.git("fetch", "--no-recurse-submodules", "origin", timeout=120)
86
+ self.revision = self.git("rev-parse", "--verify", "--end-of-options", ref + "^{commit}").decode().strip()
87
+ raw = self.git("show", self.revision + ":catalog.json")
88
+ if not SHA.fullmatch(self.revision):
89
+ raise CrowError("invalid_revision", "A referência não resolveu para um commit.")
90
+ try:
91
+ if len(raw) > 1024 * 1024:
92
+ raise ValueError("catalog too large")
93
+ data = json.loads(raw)
94
+ skills = data["skills"]
95
+ if data["version"] != 1 or not isinstance(skills, list) or len(skills) > 1000:
96
+ raise ValueError("invalid schema")
97
+ self.catalog = {}
98
+ owners = set()
99
+ for item in skills:
100
+ name = valid_name(item["name"])
101
+ if (name in self.catalog or not isinstance(item["description"], str) or
102
+ not isinstance(item["owns"], str) or not item["owns"] or item["owns"] in owners or
103
+ item["role"] not in ("coordinator", "specialist", "reviewer") or
104
+ not isinstance(item["next"], list)):
105
+ raise ValueError("invalid entry")
106
+ owners.add(item["owns"])
107
+ self.catalog[name] = item
108
+ for item in skills:
109
+ if len(item["next"]) != len(set(item["next"])) or any(n not in self.catalog or n == item["name"] for n in item["next"]):
110
+ raise ValueError("invalid handoff")
111
+ composition = data.get("composition", {})
112
+ if not isinstance(composition, dict):
113
+ raise ValueError("invalid composition")
114
+ self.overlays = composition.get("overlays", [])
115
+ if not isinstance(self.overlays, list):
116
+ raise ValueError("invalid overlays")
117
+ for overlay in self.overlays:
118
+ if overlay["skill"] not in self.catalog or not isinstance(overlay["roles"], list) or any(r not in ("coordinator", "specialist", "reviewer") for r in overlay["roles"]):
119
+ raise ValueError("invalid overlay")
120
+ except (ValueError, KeyError, TypeError) as exc:
121
+ raise CrowError("invalid_catalog", "Catálogo inválido no commit selecionado.") from exc
122
+
123
+ def _init_bundle(self):
124
+ try:
125
+ p = self.path / "release.json"
126
+ if p.is_symlink() or p.stat().st_size > 2 * 1024 * 1024:
127
+ raise ValueError("manifest")
128
+ data = json.loads(p.read_bytes())
129
+ if data["schema_version"] != 1 or not SHA.fullmatch(data["revision"]) or not isinstance(data["files"], dict) or len(data["files"]) > 40000:
130
+ raise ValueError("schema")
131
+ for name, entry in data["files"].items():
132
+ safe_relative(name)
133
+ if (not isinstance(entry, dict) or set(entry) != {"kind", "sha256", "executable"} or entry["kind"] != "file" or
134
+ not isinstance(entry["sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", entry["sha256"]) or not isinstance(entry["executable"], bool)):
135
+ raise ValueError("entry")
136
+ self.bundle = data["files"]
137
+ self.revision = data["revision"]
138
+ except (OSError, ValueError, TypeError, KeyError) as exc:
139
+ raise CrowError("invalid_release", "Manifesto do snapshot npm inválido.") from exc
140
+
141
+ def bundle_file(self, name):
142
+ safe_relative(name)
143
+ try:
144
+ p = self.path / name
145
+ for item in (p, *p.parents):
146
+ if item == self.path:
147
+ break
148
+ if item.is_symlink():
149
+ raise ValueError("symlink")
150
+ info = p.stat()
151
+ if not stat.S_ISREG(info.st_mode) or info.st_size > MAX_FILE:
152
+ raise ValueError("size/type")
153
+ data = p.read_bytes()
154
+ if file_entry(data, bool(info.st_mode & 0o111)) != self.bundle[name]:
155
+ raise ValueError("digest/mode")
156
+ return data
157
+ except (OSError, KeyError, ValueError) as exc:
158
+ raise CrowError("invalid_release", "Recurso ausente, alterado ou inseguro no snapshot npm.") from exc
159
+
160
+ def git(self, *args, timeout=30):
161
+ try:
162
+ result = subprocess.run(["git", "--no-replace-objects", "-c", "core.hooksPath=/dev/null", "-c", "gc.auto=0",
163
+ "-c", "protocol.ext.allow=never", "-C", str(self.path), *args],
164
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
165
+ except (OSError, subprocess.TimeoutExpired) as exc:
166
+ raise CrowError("git_unavailable", "Git indisponível ou operação excedeu o tempo limite.") from exc
167
+ if result.returncode:
168
+ # Git/auth helpers can print credentials: never echo their stderr.
169
+ raise CrowError("git_failed", "Git falhou. Verifique clone, referência e acesso ao origin; stderr omitido para proteger credenciais.")
170
+ return result.stdout
171
+
172
+ def related(self, name):
173
+ self.require(name)
174
+ return {
175
+ "handoffs": list(self.catalog[name]["next"]),
176
+ "used_by": [n for n, v in self.catalog.items() if name in v["next"]],
177
+ "behaviour_overlays": [v["skill"] for v in self.overlays
178
+ if v["skill"] != name and self.catalog[name]["role"] in v["roles"]],
179
+ "can_coordinate": [n for v in self.overlays if v["skill"] == name
180
+ for n, item in self.catalog.items() if n != name and item["role"] in v["roles"]],
181
+ "optional": True,
182
+ }
183
+
184
+ def require(self, name):
185
+ valid_name(name)
186
+ if name not in self.catalog:
187
+ raise CrowError("unknown_skill", "Skill não encontrada no catálogo selecionado: " + name)
188
+
189
+ def _git_files(self, name):
190
+ prefix = "skills/" + name + "/"
191
+ raw = self.git("ls-tree", "-r", "-z", "-l", self.revision, "--", prefix)
192
+ files = {}
193
+ total = 0
194
+ for record in raw.split(b"\0"):
195
+ if not record:
196
+ continue
197
+ try:
198
+ header, path = record.split(b"\t", 1)
199
+ mode, kind, oid, size = header.split()
200
+ path = path.decode("utf-8")
201
+ if not path.startswith(prefix):
202
+ raise ValueError("prefix")
203
+ rel = path[len(prefix):]
204
+ safe_relative(rel)
205
+ if mode not in (b"100644", b"100755") or kind != b"blob":
206
+ raise CrowError("unsafe_source", "Symlinks, submódulos e arquivos especiais não são instaláveis.")
207
+ size = int(size)
208
+ total += size
209
+ if size > MAX_FILE or total > MAX_PACKAGE or len(files) >= MAX_FILES:
210
+ raise CrowError("package_limit", "Pacote excede limite de tamanho/arquivos.")
211
+ blob = self.git("cat-file", "blob", oid.decode())
212
+ if len(blob) != size:
213
+ raise ValueError("size")
214
+ files[rel] = (blob, mode == b"100755")
215
+ except (ValueError, UnicodeError) as exc:
216
+ raise CrowError("unsafe_source", "Inventário de pacote inválido.") from exc
217
+ return files
218
+
219
+ def package(self, name):
220
+ self.require(name)
221
+ if self.bundle is None:
222
+ files = self._git_files(name)
223
+ else:
224
+ prefix = "skills/" + name + "/"
225
+ files = {}
226
+ total = 0
227
+ for path, entry in self.bundle.items():
228
+ if not path.startswith(prefix):
229
+ continue
230
+ data = self.bundle_file(path)
231
+ total += len(data)
232
+ if total > MAX_PACKAGE or len(files) >= MAX_FILES:
233
+ raise CrowError("package_limit", "Snapshot excede limite de tamanho/arquivos.")
234
+ files[path[len(prefix):]] = (data, entry["executable"])
235
+ if "SKILL.md" not in files:
236
+ raise CrowError("invalid_package", "SKILL.md ausente do pacote.")
237
+ try:
238
+ text = files["SKILL.md"][0].decode("utf-8")
239
+ head, sep, _ = text[4:].partition("\n---\n")
240
+ values = dict(line.split(":", 1) for line in head.splitlines())
241
+ if not text.startswith("---\n") or not sep or values["name"].strip() != name or json.loads(values["description"].strip()) != self.catalog[name]["description"]:
242
+ raise ValueError("frontmatter mismatch")
243
+ # Parse only the repository's documented simple YAML subset, never execute it.
244
+ for rel, (content, _) in files.items():
245
+ if rel.endswith(".md"):
246
+ for link in re.findall(r"!?\[[^\]]*\]\(([^)\s]+)\)", content.decode("utf-8")):
247
+ if "://" in link or link.startswith(("#", "mailto:")):
248
+ continue
249
+ import posixpath
250
+ joined = posixpath.normpath(posixpath.join(posixpath.dirname(rel), link.split("#", 1)[0]))
251
+ safe_relative(joined)
252
+ if joined not in files:
253
+ raise ValueError("missing reference")
254
+ except (ValueError, KeyError, UnicodeError) as exc:
255
+ raise CrowError("invalid_package", "Frontmatter/referências do pacote não correspondem ao catálogo.") from exc
256
+ return Package(name, files, entries_for(files))