@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,396 @@
1
+ """Managed copies, local-edit protection and recoverable multi-target transactions.
2
+
3
+ Locks coordinate this CLI's writers. They are not a sandbox against a hostile
4
+ process running as the same OS user; agent runtimes must not mutate a target
5
+ concurrently. Backups live beside, never inside, the skill discovery directory.
6
+ """
7
+ from contextlib import contextmanager
8
+ from datetime import datetime, timezone
9
+ import hashlib
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ import re
14
+ import shutil
15
+ import stat
16
+ import uuid
17
+
18
+ from .source import CrowError, MAX_FILE, MAX_PACKAGE, MAX_FILES, SHA, digest, file_entry, safe_relative, valid_name
19
+
20
+
21
+ def checked(path):
22
+ path = Path(os.path.abspath(Path(path).expanduser()))
23
+ for part in reversed((path, *path.parents)):
24
+ if part.is_symlink():
25
+ raise CrowError("unsafe_destination", "Symlink no destino/estado; use um caminho físico explícito.")
26
+ return path
27
+
28
+
29
+ def ensure_dir(path, mode=0o700):
30
+ path = checked(path)
31
+ path.mkdir(mode=mode, parents=True, exist_ok=True)
32
+ if not path.is_dir():
33
+ raise CrowError("unsafe_destination", "Diretório esperado no destino/estado.")
34
+ return path
35
+
36
+
37
+ def private_dir(path):
38
+ path = ensure_dir(path)
39
+ if path.stat().st_uid != os.getuid():
40
+ raise CrowError("unsafe_state", "Diretório reservado pertence a outro usuário.")
41
+ path.chmod(0o700)
42
+ return path
43
+
44
+
45
+ def snapshot(path):
46
+ """Hash complete trees including empty dirs; reject links/special files."""
47
+ path = checked(path)
48
+ if not path.exists():
49
+ return None
50
+ if not path.is_dir():
51
+ raise CrowError("unsafe_destination", "O destino de uma skill não é um diretório regular.")
52
+ result, total = {}, 0
53
+ for root, dirs, files in os.walk(path, followlinks=False):
54
+ for name in sorted(dirs + files):
55
+ p = Path(root) / name
56
+ rel = p.relative_to(path).as_posix()
57
+ safe_relative(rel)
58
+ info = p.lstat()
59
+ if stat.S_ISDIR(info.st_mode):
60
+ result[rel] = {"kind": "dir"}
61
+ elif stat.S_ISREG(info.st_mode):
62
+ total += info.st_size
63
+ if info.st_size > MAX_FILE or total > MAX_PACKAGE or len(result) >= MAX_FILES * 2:
64
+ raise CrowError("package_limit", "Destino excede os limites de leitura segura.")
65
+ result[rel] = file_entry(p.read_bytes(), bool(info.st_mode & 0o111))
66
+ else:
67
+ raise CrowError("unsafe_destination", "Symlink ou arquivo especial encontrado no pacote instalado.")
68
+ return dict(sorted(result.items()))
69
+
70
+
71
+ def atomic_json(path, data):
72
+ path = checked(path)
73
+ ensure_dir(path.parent)
74
+ temp = path.parent / ("." + path.name + "." + uuid.uuid4().hex)
75
+ try:
76
+ with temp.open("x", encoding="utf-8") as f:
77
+ os.chmod(temp, 0o600)
78
+ json.dump(data, f, ensure_ascii=False, sort_keys=True, indent=2)
79
+ f.write("\n")
80
+ f.flush()
81
+ os.fsync(f.fileno())
82
+ os.replace(temp, path)
83
+ # Persist directory entries on POSIX as well as JSON file contents.
84
+ fd = os.open(path.parent, os.O_RDONLY)
85
+ try:
86
+ os.fsync(fd)
87
+ finally:
88
+ os.close(fd)
89
+ finally:
90
+ temp.unlink(missing_ok=True)
91
+
92
+
93
+ def read_json(path):
94
+ path = checked(path)
95
+ try:
96
+ if not path.is_file() or path.stat().st_size > 16 * 1024 * 1024:
97
+ raise ValueError("size/type")
98
+ return json.loads(path.read_text(encoding="utf-8"))
99
+ except (ValueError, OSError) as exc:
100
+ raise CrowError("invalid_state", "Estado/registro transacional inválido; nenhuma alteração aplicada.") from exc
101
+
102
+
103
+ def validate_entries(entries):
104
+ if not isinstance(entries, dict) or len(entries) > MAX_FILES * 2:
105
+ raise CrowError("invalid_state", "Inventário inválido no estado.")
106
+ for name, value in entries.items():
107
+ safe_relative(name)
108
+ if not isinstance(value, dict):
109
+ raise CrowError("invalid_state", "Registro de arquivo inválido.")
110
+ if value == {"kind": "dir"}:
111
+ continue
112
+ if (set(value) != {"kind", "sha256", "executable"} or value["kind"] != "file" or
113
+ not isinstance(value["sha256"], str) or not re.fullmatch(r"[a-f0-9]{64}", value["sha256"]) or
114
+ not isinstance(value["executable"], bool)):
115
+ raise CrowError("invalid_state", "Hash/tipo de arquivo inválido no estado.")
116
+
117
+
118
+ def validate_state(data, target):
119
+ try:
120
+ if (data["schema_version"] != 1 or data["target"] != str(target) or
121
+ not isinstance(data["installed"], dict) or len(data["installed"]) > 1000):
122
+ raise ValueError("schema")
123
+ for name, record in data["installed"].items():
124
+ valid_name(name)
125
+ validate_entries(record["entries"])
126
+ if not SHA.fullmatch(record["revision"]) or digest(record["entries"]) != record["package_sha256"]:
127
+ raise ValueError("hash")
128
+ except (KeyError, TypeError, ValueError) as exc:
129
+ raise CrowError("invalid_state", "Manifesto inválido ou associado a outro destino.") from exc
130
+ return data
131
+
132
+
133
+ class Store:
134
+ def __init__(self, home):
135
+ self.home = checked(home)
136
+ self.root = self.home / ".local/state/crow-skills"
137
+
138
+ def state_path(self, target):
139
+ key = hashlib.sha256(str(checked(target)).encode()).hexdigest()
140
+ return self.root / "targets" / key / "state.json"
141
+
142
+ def state(self, target):
143
+ target = checked(target)
144
+ path = checked(self.state_path(target))
145
+ if not path.exists():
146
+ return {"schema_version": 1, "target": str(target), "installed": {}}
147
+ return validate_state(read_json(path), target)
148
+
149
+ def transactions(self):
150
+ directory = checked(self.root / "transactions")
151
+ return [self.journal(p.stem) for p in sorted(directory.glob("*.json"))] if directory.exists() else []
152
+
153
+ def journal(self, txid):
154
+ if not re.fullmatch(r"[a-f0-9]{32}", txid):
155
+ raise CrowError("invalid_transaction", "ID de transação inválido.")
156
+ data = read_json(self.root / "transactions" / (txid + ".json"))
157
+ try:
158
+ if data["id"] != txid or data["phase"] not in ("prepared", "restoring", "committed", "rolled_back"):
159
+ raise ValueError("phase")
160
+ targets = data["before"]
161
+ if not isinstance(targets, dict) or set(targets) != set(data["after"]):
162
+ raise ValueError("targets")
163
+ for target, before in targets.items():
164
+ if str(checked(target)) != target:
165
+ raise ValueError("path")
166
+ validate_state(before, target)
167
+ validate_state(data["after"][target], target)
168
+ for item in data["items"]:
169
+ if item["target"] not in targets:
170
+ raise ValueError("item target")
171
+ valid_name(item["name"])
172
+ for key in ("old", "new"):
173
+ if item[key] is not None:
174
+ validate_entries(item[key])
175
+ except (ValueError, KeyError, TypeError) as exc:
176
+ raise CrowError("invalid_transaction", "Registro transacional inconsistente; recuperação recusada.") from exc
177
+ return data
178
+
179
+ def pending(self):
180
+ return [t for t in self.transactions() if t["phase"] in ("prepared", "restoring")]
181
+
182
+ @contextmanager
183
+ def lock(self):
184
+ try:
185
+ import fcntl
186
+ except ImportError as exc:
187
+ raise CrowError("unsupported_platform", "Mutações requerem locks POSIX (macOS/Linux).") from exc
188
+ private_dir(self.root)
189
+ path = checked(self.root / "lock")
190
+ fd = os.open(path, os.O_RDWR | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0), 0o600)
191
+ try:
192
+ try:
193
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
194
+ except BlockingIOError as exc:
195
+ raise CrowError("busy", "Outra operação crow-skills está em execução neste perfil.") from exc
196
+ yield
197
+ finally:
198
+ os.close(fd)
199
+
200
+ @staticmethod
201
+ def work(target, txid):
202
+ target = checked(target)
203
+ key = hashlib.sha256(str(target).encode()).hexdigest()[:20]
204
+ return checked(target.parent / ".crow-skills" / key / txid)
205
+
206
+ def plan(self, command, names, targets, source=None, replace=False, overwrite=False):
207
+ if self.pending():
208
+ raise CrowError("recovery_required", "Há uma transação interrompida. Execute recover antes de alterar instalações.")
209
+ packages, before, after, items, output = {}, {}, {}, [], []
210
+ for target_info in targets:
211
+ target = checked(target_info["path"])
212
+ if target.exists() and not target.is_dir():
213
+ raise CrowError("unsafe_destination", "Raiz de skills não é diretório.")
214
+ if target == self.home or target == Path(target.anchor) or self.root.is_relative_to(target):
215
+ raise CrowError("unsafe_destination", "Destino amplo demais ou contém o estado do instalador.")
216
+ if source and target.is_relative_to(source.path):
217
+ raise CrowError("unsafe_destination", "Não instale sobre o próprio repositório fonte.")
218
+ state = self.state(target)
219
+ key = str(target)
220
+ before[key] = state
221
+ after[key] = json.loads(json.dumps(state))
222
+ selected = list(names) if names else list(state["installed"])
223
+ for name in selected:
224
+ valid_name(name)
225
+ record = state["installed"].get(name)
226
+ current = snapshot(target / name)
227
+ if command != "install" and record is None:
228
+ raise CrowError("not_managed", "Skill não gerenciada neste destino: " + name)
229
+ if record and current != record["entries"] and not overwrite:
230
+ raise CrowError("local_changes", "Skill modificada/ausente: " + name + ". Revise; --overwrite-modified autoriza preservar backup e substituir/remover.")
231
+ if not record and current is not None and not replace:
232
+ raise CrowError("unmanaged_conflict", "Destino já existe e não é gerenciado: " + name + ". Use --replace após revisar.")
233
+ new = None
234
+ if command != "uninstall":
235
+ if record and command == "update" and record.get("source") != str(source.path):
236
+ raise CrowError("source_mismatch", "Update requer a mesma origem; selecione --source explicitamente.")
237
+ if name not in packages:
238
+ packages[name] = source.package(name)
239
+ new = packages[name].entries
240
+ if (record and command != "uninstall" and current == new
241
+ and record["revision"] == source.revision and record.get("source") == str(source.path)):
242
+ output.append({"name": name, "target": key, "agents": target_info["agents"], "action": "unchanged"})
243
+ continue
244
+ action = "remove" if command == "uninstall" else ("replace" if current is not None else "install")
245
+ items.append({"name": name, "target": key, "old": current, "new": new})
246
+ output.append({"name": name, "target": key, "agents": target_info["agents"], "action": action})
247
+ if command == "uninstall":
248
+ after[key]["installed"].pop(name)
249
+ else:
250
+ after[key]["installed"][name] = {
251
+ "revision": source.revision, "source": str(source.path), "entries": new,
252
+ "package_sha256": digest(new), "agents": target_info["agents"],
253
+ "installed_at": datetime.now(timezone.utc).isoformat(),
254
+ }
255
+ return {"before": before, "after": after, "items": items, "packages": packages, "changes": output}
256
+
257
+ def apply(self, plan):
258
+ if not plan["items"]:
259
+ return None
260
+ with self.lock():
261
+ if self.pending():
262
+ raise CrowError("recovery_required", "Recuperação pendente; nenhuma alteração aplicada.")
263
+ for target, state in plan["before"].items():
264
+ if self.state(target) != state:
265
+ raise CrowError("changed_during_plan", "O estado mudou após o preview; execute novamente.")
266
+ for item in plan["items"]:
267
+ if snapshot(Path(item["target"]) / item["name"]) != item["old"]:
268
+ raise CrowError("changed_during_plan", "Um destino mudou após o preview; execute novamente.")
269
+ txid = uuid.uuid4().hex
270
+ data = {k: plan[k] for k in ("before", "after", "items")}
271
+ data.update(id=txid, phase="prepared", created_at=datetime.now(timezone.utc).isoformat())
272
+ journal = self.root / "transactions" / (txid + ".json")
273
+ try:
274
+ for target in plan["before"]:
275
+ work = self.work(target, txid)
276
+ private_dir(work.parent.parent)
277
+ private_dir(work.parent)
278
+ work.mkdir(mode=0o700)
279
+ (work / "old").mkdir()
280
+ (work / "new").mkdir()
281
+ for item in plan["items"]:
282
+ if item["new"] is not None:
283
+ new = self.work(item["target"], txid) / "new" / item["name"]
284
+ plan["packages"][item["name"]].materialize(new)
285
+ if snapshot(new) != item["new"]:
286
+ raise CrowError("staging_failed", "Cópia staged não corresponde ao pacote validado.")
287
+ atomic_json(journal, data)
288
+ for item in plan["items"]:
289
+ target = checked(item["target"])
290
+ ensure_dir(target)
291
+ dest = target / item["name"]
292
+ work = self.work(target, txid)
293
+ if snapshot(dest) != item["old"]:
294
+ raise CrowError("changed_during_install", "Destino alterado durante instalação.")
295
+ if item["old"] is not None:
296
+ os.replace(dest, work / "old" / item["name"])
297
+ if item["new"] is not None:
298
+ os.replace(work / "new" / item["name"], dest)
299
+ for target, state in data["after"].items():
300
+ atomic_json(self.state_path(target), state)
301
+ data["phase"] = "committed"
302
+ atomic_json(journal, data)
303
+ except (Exception, KeyboardInterrupt):
304
+ if journal.exists():
305
+ # Leave the prepared journal on failure; recover will not overwrite edits.
306
+ self._restore(data)
307
+ else:
308
+ for target in plan["before"]:
309
+ work = self.work(target, txid)
310
+ if work.exists():
311
+ shutil.rmtree(work)
312
+ raise
313
+ return txid
314
+
315
+ def _restore(self, data):
316
+ """Validate every target/backup before changing any during recovery."""
317
+ operations = []
318
+ for target, before in data["before"].items():
319
+ current = self.state(target)
320
+ if current != before and current != data["after"][target]:
321
+ raise CrowError("recovery_conflict", "Manifesto mudou após a transação; recuperação recusada.")
322
+ for item in reversed(data["items"]):
323
+ dest = checked(Path(item["target"]) / item["name"])
324
+ old = self.work(item["target"], data["id"]) / "old" / item["name"]
325
+ backup = snapshot(old)
326
+ current = snapshot(dest)
327
+ if backup is not None:
328
+ if backup != item["old"] or current not in (None, item["new"]):
329
+ raise CrowError("recovery_conflict", "Backup/destino diverge; recuperação não sobrescreverá alterações locais.")
330
+ operations.append((dest, old))
331
+ elif item["old"] is None:
332
+ if current not in (None, item["new"]):
333
+ raise CrowError("recovery_conflict", "Novo destino foi alterado; recuperação recusada.")
334
+ operations.append((dest, None))
335
+ elif current != item["old"]:
336
+ raise CrowError("recovery_conflict", "Backup original ausente; recuperação recusada.")
337
+ # A rollback is itself a multi-target mutation. Persist its intent before
338
+ # the first restore, so a second interruption remains recoverable.
339
+ data["phase"] = "restoring"
340
+ atomic_json(self.root / "transactions" / (data["id"] + ".json"), data)
341
+ for dest, backup in operations:
342
+ if dest.exists():
343
+ shutil.rmtree(dest)
344
+ if backup is not None:
345
+ os.replace(backup, dest)
346
+ for target, state in data["before"].items():
347
+ atomic_json(self.state_path(target), state)
348
+ data["phase"] = "rolled_back"
349
+ atomic_json(self.root / "transactions" / (data["id"] + ".json"), data)
350
+
351
+ def rollback(self, txid, recovery=False):
352
+ with self.lock():
353
+ data = self.journal(txid)
354
+ expected = ("prepared", "restoring") if recovery else ("committed",)
355
+ if data["phase"] not in expected:
356
+ raise CrowError("invalid_transaction", "Estado da transação incompatível com a operação.")
357
+ if not recovery and self.pending():
358
+ raise CrowError("recovery_required", "Resolva transações interrompidas primeiro.")
359
+ if not recovery:
360
+ for target, state in data["after"].items():
361
+ if self.state(target) != state:
362
+ raise CrowError("rollback_conflict", "Há mudanças posteriores; rollback antigo recusado.")
363
+ self._restore(data)
364
+
365
+ def status(self, targets, names=()):
366
+ rows = []
367
+ for target_info in targets:
368
+ target = checked(target_info["path"])
369
+ if target.exists() and not target.is_dir():
370
+ rows.append({"name": None, "target": str(target), "agents": target_info["agents"],
371
+ "status": "unsafe", "revision": None, "activation_verified": False,
372
+ "note": "Raiz de skills não é diretório; instalação recusada neste destino."})
373
+ continue
374
+ state = self.state(target)
375
+ found = set(state["installed"]) | set(names)
376
+ if target.is_dir():
377
+ found.update(p.name for p in target.iterdir() if p.name.startswith("crow-"))
378
+ for name in sorted(found):
379
+ try:
380
+ valid_name(name)
381
+ except CrowError:
382
+ rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
383
+ "status": "unmanaged", "revision": None, "activation_verified": False,
384
+ "note": "Nome externo ao catálogo: não gerenciado nem removido por este CLI."})
385
+ continue
386
+ record = state["installed"].get(name)
387
+ try:
388
+ entries = snapshot(target / name)
389
+ status = ("absent" if entries is None else "unmanaged") if record is None else (
390
+ "missing" if entries is None else "installed" if entries == record["entries"] else "modified")
391
+ except CrowError:
392
+ status = "unsafe"
393
+ rows.append({"name": name, "target": str(target), "agents": target_info["agents"],
394
+ "status": status, "revision": record.get("revision") if record else None,
395
+ "activation_verified": False})
396
+ return rows
@@ -0,0 +1,33 @@
1
+ # Revisão de segurança do CLI
2
+
3
+ ## Fronteiras e controles do plano
4
+
5
+ Ativos: skills já existentes, alterações locais, scripts/recursos completos, estado de instalação, credenciais Git externas e arquivos fora dos destinos autorizados.
6
+
7
+ Fronteiras: catálogo/objetos Git são dados; escolha explícita do usuário/agente define destinos e operações; host interpreta as skills somente depois da instalação. O CLI não interpreta diretivas de SKILL.md como comandos e não executa scripts/dependency installers.
8
+
9
+ Checklist aplicável: autorização por escopo e flags (sim); validação de nomes/refs/paths e tipos (sim); saída JSON e Git sem shell (sim); segredos fora de código/logs (sim); rollback, limites e concorrência (sim); dependências Python externas (nenhuma). Auth de servidor, SQL, TLS de serviço, CORS, JWT e sessão web não se aplicam a este processo local. Git fetch usa autenticação/transportes do Git do operador e não imprime stderr; não há token próprio.
10
+
11
+ ## Caminhos de ataque e verificação
12
+
13
+ | Caminho | Controle e evidência a executar |
14
+ |---|---|
15
+ | Nome ou recurso escapando da pasta | Allowlist de nome, caminhos relativos normalizados, recusa de `..`, absoluto, controles e symlinks; testes de fonte/caminho |
16
+ | Execução de script/hook durante instalação | Leitura Git inerte, sem checkout/exec de pacote, hooks/replace-objects desabilitados; copiar script sentinela sem execução |
17
+ | Fonte diferente sob SHA aparente | Objetos do commit fixado e Git replace desabilitado; inventário/hashes completos, identidade de origem persistida |
18
+ | Apagar/substituir terceiros ou edições | Manifesto por destino, preflight, --replace/--overwrite-modified explícitos, backup; testes de conflito/edições/arquivos extras |
19
+ | Symlink/arquivo especial no destino ou estado | Checagem de componentes e scan de árvore, sem seguir links; testes de ancestor, link interno e FIFO |
20
+ | Erro no segundo agente após mudar o primeiro | Staging, journal prévio e restauração; injeção de erro no segundo rename |
21
+ | Interrupção após renames parciais | Journal preparado bloqueia novas mutações, recover valida e restaura; interrupção simulada e teste de edições pós-crash |
22
+ | Rollback antigo apagando versão nova | Comparação de estado posterior e inventário antes de restaurar; teste de transação subsequente |
23
+ | Dois instaladores concorrentes | Lock POSIX por perfil; segundo processo real deve retornar busy |
24
+ | Preview ou list instalando silenciosamente | Sem gravação/lock/fetch no dry-run, flags explícitas em mutações; testes de diretório vazio e fetch proibido |
25
+ | Metadados de complementaridade virando instalação em massa | Relacionamentos opcionais, --with-handoffs direto e explícito, sem expansão de overlays; teste de conjunto exato |
26
+
27
+ Esta tabela define ameaça/controle, não é um certificado de segurança. Resultados executados e revisão independente são registrados separadamente na entrega. Persistem limites de processos hostis do mesmo usuário, filesystem/hardware, confiança no clone/configuração Git escolhido e efeitos que o host venha a permitir ao executar uma skill após instalada.
28
+
29
+ ## Snapshot npm
30
+
31
+ O launcher usa spawn sem shell e Python isolado (`-I`), evitando import do cwd/PYTHONPATH. Não há dependências npm nem hooks de instalação. Prepack verifica bytes commitados e rejeita arquivos extras nas globs de publicação; caches Python são explicitamente excluídos. O smoke valida o tarball inteiro contra allowlist/hashes antes de instalar em perfil isolado. Checksums não provam autoria: distribuição depende da integridade/autorização do registro npm. Publicação deve ser escopada e restricted, sem fallback público.
32
+
33
+ Resultados, falhas corrigidas e limites: [avaliação do CLI](evaluations/2026-10-20-cli.md).
package/docs/cli.md ADDED
@@ -0,0 +1,138 @@
1
+ # crow-skills CLI 0.1.0
2
+
3
+ CLI local-first para humanos e agentes. Python 3.10+, sem bibliotecas Python externas; Git é necessário para fontes em clone, não para o snapshot npm. O launcher npm requer Node 18+. Mutações suportadas em macOS/Linux (locks POSIX). Não instala nem configura um agente, não executa scripts das skills e não muda modelos, prompts ou permissões.
4
+
5
+ ## Executar
6
+
7
+ No clone existente:
8
+
9
+ ```sh
10
+ ./bin/crow-skills --help
11
+ python3 -m crow_skills list
12
+ ```
13
+
14
+ O launcher do checkout pode ser referenciado por um symlink em um diretório já presente no PATH. Ele depende do caminho do checkout. Usar Git em um clone existente preserva a autenticação Git já configurada, inclusive para este repositório privado.
15
+
16
+ ### Pacote npm privado
17
+
18
+ Nome: `@andespindola/crow-skills`; `publishConfig.access` é `restricted`. Isso não equivale a `private:true`, que impediria qualquer publicação. A publicação exige autorização/entitlement da conta npm; nunca faça fallback para public.
19
+
20
+ Depois de a versão estar publicada e a conta estar autenticada com acesso:
21
+
22
+ ```sh
23
+ npm install -g @andespindola/crow-skills
24
+ crow-skills list
25
+ npx --yes @andespindola/crow-skills related crow-backend --json
26
+ ```
27
+
28
+ Para testar/distribuir o arquivo local sem registry:
29
+
30
+ ```sh
31
+ npm pack
32
+ npm install -g ./andespindola-crow-skills-0.1.0.tgz --ignore-scripts
33
+ ```
34
+
35
+ O pacote contém o CLI Python e todas as skills do commit, com `release.json` de hashes. Não requer acesso ao GitHub ou clone para list/install. O launcher Node encaminha argumentos sem shell para Python em modo isolado, evitando imports pelo diretório do chamador/PYTHONPATH. `CROW_SKILLS_PYTHON` pode apontar para outro executável Python 3.10+. Não há dependências npm nem hooks de instalação que instalem skills automaticamente.
36
+
37
+ `npm pack` roda um prepack de desenvolvimento que recusa payload não commitado e gera o snapshot. O arquivo enviado inclui somente o payload allowlisted, não análises, testes, notas, ambiente ou credenciais. A integridade do transporte/origem continua sendo responsabilidade do npm e do registro confiável; checksums locais não substituem assinatura/autenticidade.
38
+
39
+ ## Instalar
40
+
41
+ Sem argumentos, `crow-skills install` em terminal interativo mostra catálogo e pede skills, agentes e escopo; depois apresenta o plano e pede aplicação. Não assume todos os agentes nem instalação global.
42
+
43
+ ```sh
44
+ crow-skills install crow-behaviour --agents claude,codex --global --dry-run
45
+ crow-skills install crow-behaviour --agents claude,codex --global --yes
46
+ crow-skills install crow-debug --agents gemini,devin --project . --yes
47
+ crow-skills install crow-debug --dest /caminho/do/host/skills --yes
48
+ ```
49
+
50
+ `--dest` indica uma raiz de skills explícita para outro host; não promete compatibilidade/ativação nesse host. Escolha somente um escopo: `--global`, `--project PATH` ou `--dest PATH`. Não combine `--dest` com `--agents`.
51
+
52
+ `--all` seleciona explicitamente todas as skills do catálogo em install. Sem `--all`, só os nomes pedidos são instalados. Cada pacote inteiro é copiado: referências, scripts, assets e metadata, preservando hashes e executabilidade. Dependências externas não são baixadas nem executadas pelo instalador.
53
+
54
+ ## Complementaridade
55
+
56
+ ```sh
57
+ crow-skills list
58
+ crow-skills info crow-backend --agents claude --global
59
+ crow-skills related crow-behaviour --agents codex --global --json
60
+ crow-skills install crow-backend --with-handoffs --agents claude --global --dry-run
61
+ ```
62
+
63
+ O catálogo declara `role`, `owns` e `next`, com um owner por artefato. `composition.overlays` declara modos transversais, como crow-behaviour. `info`/`related` mostram handoffs de saída, consumidores, overlays aplicáveis, escopos coordenáveis e presença/integridade nos destinos selecionados. São sugestões de composição, não dependências, garantia de disponibilidade de ferramentas ou uma ordem para carregar toda a biblioteca.
64
+
65
+ `--with-handoffs` inclui apenas os `next` diretos das skills explicitamente selecionadas. Não percorre ciclos/transitividade, não instala consumidores e não expande overlays para todo o catálogo. Relevância por intenção continua sendo decisão do usuário/agente; o CLI não inventa um roteador semântico por palavras-chave.
66
+
67
+ ## Origem e versões
68
+
69
+ ```sh
70
+ crow-skills list --source /caminho/do/clone --ref main --json
71
+ crow-skills install crow-debug --agents claude --global --ref COMMIT_OU_TAG --yes
72
+ crow-skills update --agents claude --global --fetch --ref origin/main --yes
73
+ ```
74
+
75
+ A fonte em checkout deve ser um clone Git já existente. O padrão é o checkout que contém o CLI e `--ref HEAD`. Toda operação fixa a referência em SHA completo e lê os objetos desse commit, sem checkout e sem arquivos não commitados. Na distribuição npm, o padrão é o snapshot incluído, verificado por hashes e associado a esse SHA; outras refs/fetch exigem `--source` apontando para clone Git. O CLI não cria clones, worktrees ou repos temporários.
76
+
77
+ `--fetch` busca `origin` somente quando solicitado, usando credenciais Git externas; não atualiza o checkout. É incompatível com `--dry-run`. Sem fetch, uma ref remota pode estar desatualizada. Git replace-objects e hooks não são usados para interpretar a fonte; erros de Git não imprimem stderr que possa conter credenciais. URL arbitrária/credencial não é argumento suportado de `--source`.
78
+
79
+ Atualização é explícita. `update` sem nomes atualiza apenas instalações gerenciadas nos destinos selecionados; não instala o catálogo inteiro. A origem deve corresponder à registrada. Mudança intencional de origem requer install/revisão explícita, não troca silenciosa durante update.
80
+
81
+ No npm, primeiro atualize o pacote npm e então execute o update das skills, ou escolha uma fonte Git explicitamente. Com npx, o caminho de cache pode mudar entre versões; nesse caso, use install explícito para revisar/registrar a nova origem em vez de contornar source_mismatch.
82
+
83
+ ## Estado, conflitos e recuperação
84
+
85
+ ```sh
86
+ crow-skills status --agents claude,codex --global
87
+ crow-skills doctor --agents claude,codex --global --json
88
+ crow-skills uninstall crow-debug --agents claude --global --yes
89
+ crow-skills rollback --transaction ID_RETORNADO --dry-run
90
+ crow-skills rollback --transaction ID_RETORNADO --yes
91
+ crow-skills recover --dry-run
92
+ crow-skills recover --yes
93
+ ```
94
+
95
+ - Manifestos/locks/journals ficam sob `~/.local/state/crow-skills/`, separados da descoberta de skills. Não se escreve `manifest.json` dentro de `.claude/skills`.
96
+ - Staging e backups ficam no diretório reservado `.crow-skills/` ao lado da raiz de skills, na mesma filesystem para permitir renames, nunca dentro dela. Backups são privados e retidos; não há expiração automática nesta versão.
97
+ - Todos os destinos passam por preflight antes de aplicar. Renames são atômicos por pasta; uma operação com múltiplos destinos não é um único rename atômico global. Erros comuns provocam restauração; interrupções deixam journal recuperável, bloqueando novas mutações até `recover`.
98
+ - `recover` restaura o estado anterior, não tenta terminar silenciosamente a instalação. A própria restauração tem um estado pendente `restoring`, permitindo retomada se também for interrompida. Confere arquivos/backup antes de substituir. Se houver edições após a interrupção, recusa e conserva os dados para resolução manual.
99
+ - `rollback` funciona sobre uma transação concluída sem alterações posteriores no estado/artefatos afetados. Uma transação antiga não sobrescreve uma nova.
100
+ - Um destino não gerenciado exige `--replace` em install. Edições locais, arquivos extras ou ausência de pacote gerenciado bloqueiam update/uninstall. `--overwrite-modified` é autorização explícita para preservar backup e substituir/remover o pacote afetado.
101
+ - Uninstall só remove nomes gerenciados. Sem nomes exige `--all` explícito. Não elimina skills de terceiros nem diretórios alheios.
102
+ - Symlinks nos destinos/estado, recursos com travessia, submódulos, symlinks de fonte e arquivos especiais são recusados. Use caminhos físicos. Esta versão não oferece instalação por symlink.
103
+
104
+ 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.
105
+
106
+ ## Automação por agente
107
+
108
+ ```sh
109
+ crow-skills install crow-behaviour --agents claude,codex --global --non-interactive --yes --json
110
+ ```
111
+
112
+ Flags comuns funcionam antes ou depois do subcomando. `--json` implica ausência de prompts. Mutações não interativas exigem `--yes`; caso contrário, retornam erro sem instalar. `--dry-run` faz preview sem gravar estado nem instalar; ele não é autorização para aplicar depois sem nova checagem.
113
+
114
+ Envelope stdout estável:
115
+
116
+ ```json
117
+ {"schema_version":1,"ok":true,"command":"install","data":{"applied":true}}
118
+ ```
119
+
120
+ Erros retornam `ok:false` e `error.code/message`. Saídas reais incluem alterações, SHA, transação e `activation_verified:false`; o exemplo acima abrevia esses campos. Exit codes: 0 sucesso/preview, 1 conflito/falha operacional, 2 uso inválido, 130 interrupção. Doctor retorna `healthy:false` para instalações alteradas/ausentes/inseguras ou recuperação pendente; o agente deve verificar esse campo, não apenas o exit code.
121
+
122
+ `--home PATH` seleciona um perfil isolado para testes/ambientes alternativos, incluindo estado e destinos globais. Não é sandbox: `--dest` e `--project` continuam sendo caminhos explícitos fora desse perfil, se fornecidos.
123
+
124
+ ## Adaptadores e descoberta
125
+
126
+ | Adaptador | Usuário | Projeto |
127
+ |---|---|---|
128
+ | codex | `~/.agents/skills` | `.agents/skills` |
129
+ | claude | `~/.claude/skills` | `.claude/skills` |
130
+ | gemini | `~/.gemini/skills` | `.gemini/skills` |
131
+ | devin | `~/.config/devin/skills` (ou XDG_CONFIG_HOME) | `.devin/skills` |
132
+ | shared | `~/.agents/skills` | `.agents/skills` |
133
+
134
+ Codex/shared são deduplicados quando apontam para o mesmo diretório. O diretório compartilhado também pode ser lido por outros hosts; não é isolamento entre agentes. `.codex/skills` é uma localização legada encontrada em ambientes existentes; o adaptador usa a localização moderna documentada. Para um destino legado específico, use `--dest` explicitamente.
135
+
136
+ `agents` só verifica PATH e caminhos, sem iniciar agente/LLM. `status`/`doctor` conferem arquivos e estado de instalação, não provam carregamento, ativação, independência dos workers ou execução correta da skill. Gemini oferece `/skills reload`; para outros hosts, confira sua lista/reload ou reinicie a sessão conforme a versão. Nenhum hook ou system prompt é alterado para forçar uso.
137
+
138
+ Fontes primárias consultadas em 2026-10-20: [Codex](https://learn.chatgpt.com/docs/build-skills#where-codex-loads-local-skills), [Claude](https://code.claude.com/docs/en/skills), [Gemini](https://geminicli.com/docs/cli/using-agent-skills/), [Devin](https://docs.devin.ai/cli/extensibility/skills/overview).
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@andespindola/crow-skills",
3
+ "version": "0.1.0",
4
+ "description": "Versioned, complementary agent skills with a protected multi-agent installer",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+ssh://git@github.com/andersonflima/crow-skills.git"
9
+ },
10
+ "bin": {
11
+ "crow-skills": "bin/npm-crow-skills.cjs"
12
+ },
13
+ "files": [
14
+ "bin/npm-crow-skills.cjs",
15
+ "crow_skills/*.py",
16
+ "catalog.json",
17
+ "skills/**",
18
+ "release.json",
19
+ "docs/cli.md",
20
+ "docs/cli-security.md",
21
+ "!**/__pycache__/**",
22
+ "!**/*.pyc"
23
+ ],
24
+ "scripts": {
25
+ "prepack": "python3 scripts/build_release.py",
26
+ "test": "python3 scripts/check.py && python3 -m unittest discover -s tests -v"
27
+ },
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "publishConfig": {
32
+ "access": "restricted",
33
+ "registry": "https://registry.npmjs.org/"
34
+ }
35
+ }