@andespindola/crow-skills 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/catalog.json +1 -1
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/cli.py +153 -8
- package/crow_skills/guide.py +585 -0
- package/crow_skills/storage.py +6 -0
- package/docs/cli-security.md +2 -0
- package/docs/cli.md +46 -4
- package/package.json +1 -1
- package/release.json +16 -11
- package/skills/crow-behaviour/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Exemplos:
|
|
|
25
25
|
|
|
26
26
|
## Instalação e portabilidade
|
|
27
27
|
|
|
28
|
-
O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm privada `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente.
|
|
28
|
+
O pacote de cada skill é somente sua pasta em `skills/`. O [CLI crow-skills](docs/cli.md) instala cópias completas e versionadas em agentes/escopos explícitos, com preview, saída JSON, detecção de conflitos e recuperação. Requer Python 3.10+; Git para fontes em clone, Node 18+ para o launcher npm; mutações em macOS/Linux. A distribuição npm privada `@andespindola/crow-skills` inclui um snapshot completo das skills, sem clone ou hooks que as instalem automaticamente. Em `--global`, instalar a crow-behaviour também grava o guia aditivo do agente (claude, codex, gemini, devin) para usar as skills; `--no-guide` desliga.
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
31
|
./bin/crow-skills list
|
package/catalog.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"name": "crow-behaviour",
|
|
14
14
|
"title": "Atuação coordenada",
|
|
15
|
-
"description": "
|
|
15
|
+
"description": "Use PROATIVAMENTE, sem o usuário pedir, sempre que a tarefa for substantiva — multi-etapa, com exploração/decisões, mexendo em vários arquivos ou componentes, ou exigindo verificação antes de entregar (implementar uma feature, refatorar, depurar, auditar, revisar, migrar, pesquisar a fundo). Ao ativar: decomponha o trabalho, delegue com as capacidades reais do runtime, verifique de forma adversarial e sintetize evidências. NÃO ative em conversa, explicação curta ou ajuste trivial/local — nesses casos responda direto. Não simule agentes nem altere permissões.",
|
|
16
16
|
"role": "coordinator",
|
|
17
17
|
"owns": "execution-ledger",
|
|
18
18
|
"next": []
|
package/crow_skills/__init__.py
CHANGED
package/crow_skills/cli.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""Human and agent CLI. Noninteractive mutations require explicit --yes."""
|
|
2
2
|
import argparse
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
import sys
|
|
6
7
|
|
|
7
8
|
from . import __version__
|
|
9
|
+
from . import guide
|
|
8
10
|
from .agents import AGENTS, destinations, inventory
|
|
9
11
|
from .source import CrowError, Source, valid_name
|
|
10
12
|
from .storage import Store, checked
|
|
11
13
|
|
|
12
14
|
DEFAULT_SOURCE = Path(__file__).resolve().parents[1]
|
|
13
|
-
MUTATIONS = {"install", "update", "uninstall", "rollback", "recover"}
|
|
15
|
+
MUTATIONS = {"install", "update", "uninstall", "rollback", "recover", "guide"}
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class Parser(argparse.ArgumentParser):
|
|
@@ -51,6 +53,11 @@ def parser():
|
|
|
51
53
|
child.add_argument("--replace", action="store_true", help="Autorizar substituição com backup de pacote não gerenciado")
|
|
52
54
|
child.add_argument("--with-handoffs", action="store_true", help="Incluir apenas handoffs diretos opcionais do catálogo")
|
|
53
55
|
child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição/remoção de edições locais")
|
|
56
|
+
child.add_argument("--no-guide", action="store_true", help="Não aplicar/atualizar/remover o guia do agente (--global)")
|
|
57
|
+
child = sub.add_parser("guide", parents=[common], help="Instruções gerenciadas para o agente usar as skills instaladas")
|
|
58
|
+
child.add_argument("action", choices=("install", "status", "uninstall"))
|
|
59
|
+
child.add_argument("--replace", action="store_true", help="Autorizar backup e substituição de conteúdo não gerenciado no caminho do guia")
|
|
60
|
+
child.add_argument("--overwrite-modified", action="store_true", help="Autorizar backup e substituição/remoção de guia editado")
|
|
54
61
|
for name in ("rollback", "recover"):
|
|
55
62
|
child = sub.add_parser(name, parents=[common])
|
|
56
63
|
child.add_argument("--transaction", required=name == "rollback")
|
|
@@ -113,6 +120,119 @@ def relationship_view(source, name, store, targets):
|
|
|
113
120
|
"meaning": "Handoffs e overlays são complementos opcionais, não dependências nem ordem automática de execução."}
|
|
114
121
|
|
|
115
122
|
|
|
123
|
+
def guide_command(args, home, store, interactive):
|
|
124
|
+
if getattr(args, "project", None) or getattr(args, "dest", None):
|
|
125
|
+
raise CrowError("unsupported_guide", "Guias só têm mecanismo nativo verificado no escopo do usuário; use --global. "
|
|
126
|
+
"Escopo de projeto e --dest não são suportados.")
|
|
127
|
+
agents = getattr(args, "agents", None)
|
|
128
|
+
if args.action == "status":
|
|
129
|
+
rows = guide.status(store, home, guide.select(agents) if agents else list(guide.MECHANISMS))
|
|
130
|
+
return {"guides": rows, "pending_transactions": [t["id"] for t in store.pending()], "activation_verified": False}
|
|
131
|
+
if not getattr(args, "global_scope", False):
|
|
132
|
+
raise CrowError("usage", "guide " + args.action + " exige --global explícito (único escopo verificado).")
|
|
133
|
+
if not agents and interactive:
|
|
134
|
+
agents = prompt("Agentes separados por vírgula (" + ",".join(guide.MECHANISMS) + "):")
|
|
135
|
+
selected = guide.select(agents)
|
|
136
|
+
source = None
|
|
137
|
+
if args.action == "install":
|
|
138
|
+
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"), getattr(args, "fetch", False))
|
|
139
|
+
plan = guide.plan(store, args.action, selected, home, source, args.replace, args.overwrite_modified)
|
|
140
|
+
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
141
|
+
"additive": True, "activation_verified": False}
|
|
142
|
+
if getattr(args, "dry_run", False):
|
|
143
|
+
summary["preview"] = plan["previews"]
|
|
144
|
+
if plan["items"]:
|
|
145
|
+
authorize(args, interactive, summary)
|
|
146
|
+
txid = None if getattr(args, "dry_run", False) else guide.apply(store, plan, home)
|
|
147
|
+
return {**summary, "transaction": txid, "dry_run": getattr(args, "dry_run", False),
|
|
148
|
+
"applied": not getattr(args, "dry_run", False),
|
|
149
|
+
"reload_hint": ("Somente preview; nada gravado." if getattr(args, "dry_run", False) else
|
|
150
|
+
"Abra uma sessão nova do CLI para carregar as instruções; o carregamento não é verificado por este comando.")}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
GUIDE_HINT = (" O guia do agente é aplicado junto com as skills em --global; revise e use --replace ou "
|
|
154
|
+
"--overwrite-modified conforme o caso, ou --no-guide para não alterar o guia. Nada foi alterado.")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def guide_sync(args, home, store, command, targets, plan, source):
|
|
158
|
+
"""Plan the guide changes that follow a skills plan, before anything is written.
|
|
159
|
+
|
|
160
|
+
Returns (report, guide plan or None). A refused guide raises, so the command
|
|
161
|
+
fails as a whole with no partial state.
|
|
162
|
+
"""
|
|
163
|
+
report = {"changes": [], "skipped": [], "transaction": None}
|
|
164
|
+
selected = [a for t in targets for a in t["agents"]]
|
|
165
|
+
if getattr(args, "no_guide", False):
|
|
166
|
+
report["skipped"] = [{"agent": a, "reason": "--no-guide: guia não alterado."} for a in selected]
|
|
167
|
+
return report, None
|
|
168
|
+
if getattr(args, "project", None) or getattr(args, "dest", None):
|
|
169
|
+
report["skipped"] = [{"agent": a, "reason": "Sem mecanismo de guia verificado fora do escopo do usuário; "
|
|
170
|
+
"guia aplicado só com --global."} for a in selected]
|
|
171
|
+
return report, None
|
|
172
|
+
for agent in selected:
|
|
173
|
+
if agent not in guide.MECHANISMS:
|
|
174
|
+
report["skipped"].append({"agent": agent, "reason": guide.UNVERIFIED.get(agent, "Sem mecanismo de guia verificado.")})
|
|
175
|
+
state = guide.load_state(store)
|
|
176
|
+
touched = {}
|
|
177
|
+
for item in plan["items"]:
|
|
178
|
+
added, removed = touched.setdefault(os.path.abspath(item["target"]), (set(), set()))
|
|
179
|
+
(removed if item["new"] is None else added).add(item["name"])
|
|
180
|
+
actions, skills_after = {}, {}
|
|
181
|
+
for agent, mech in guide.MECHANISMS.items():
|
|
182
|
+
path = os.path.abspath(destinations(agent, home)[0]["path"])
|
|
183
|
+
record = state["guides"].get(agent)
|
|
184
|
+
if agent not in selected and not (record and path in touched):
|
|
185
|
+
continue
|
|
186
|
+
added, removed = touched.get(path, (set(), set()))
|
|
187
|
+
after = sorted((set(guide.managed_skills(agent, home, store)) | added) - removed)
|
|
188
|
+
if mech["kind"] == "toml" and guide.tomllib is None:
|
|
189
|
+
report["skipped"].append({"agent": agent, "reason": "Python 3.11+ (tomllib) é necessário para editar config.toml; guia não alterado."})
|
|
190
|
+
elif "crow-behaviour" in after and (record or command != "uninstall"):
|
|
191
|
+
actions[agent], skills_after[agent] = "install", after
|
|
192
|
+
elif record and "crow-behaviour" in removed:
|
|
193
|
+
actions[agent] = "uninstall"
|
|
194
|
+
elif record and "crow-behaviour" not in after:
|
|
195
|
+
report["skipped"].append({"agent": agent, "reason": "crow-behaviour ausente; guia mantido. Confira crow-skills guide status."})
|
|
196
|
+
elif not record and command != "uninstall":
|
|
197
|
+
report["skipped"].append({"agent": agent, "reason": "crow-behaviour não ficará instalada para " + agent + "; guia não aplicado."})
|
|
198
|
+
if not actions:
|
|
199
|
+
return report, None
|
|
200
|
+
if source is None and "install" in actions.values():
|
|
201
|
+
source = Source(getattr(args, "source", DEFAULT_SOURCE), getattr(args, "ref", "HEAD"))
|
|
202
|
+
try:
|
|
203
|
+
gplan = guide.plan(store, actions, list(actions), home, source, getattr(args, "replace", False),
|
|
204
|
+
args.overwrite_modified, skills_after)
|
|
205
|
+
except CrowError as exc:
|
|
206
|
+
raise CrowError(exc.code, str(exc) + GUIDE_HINT) from exc
|
|
207
|
+
report["changes"] = gplan["changes"]
|
|
208
|
+
report["revision"] = source.revision if source else None
|
|
209
|
+
if getattr(args, "dry_run", False):
|
|
210
|
+
report["preview"] = gplan["previews"]
|
|
211
|
+
return report, gplan
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def apply_chain(store, plan, gplan, home):
|
|
215
|
+
"""Skills first, then the guide; a failed guide rolls the skills back."""
|
|
216
|
+
txid = store.apply(plan)
|
|
217
|
+
if gplan is None:
|
|
218
|
+
return txid, None
|
|
219
|
+
try:
|
|
220
|
+
return txid, guide.apply(store, gplan, home, linked=txid)
|
|
221
|
+
except (Exception, KeyboardInterrupt) as exc:
|
|
222
|
+
if txid:
|
|
223
|
+
try:
|
|
224
|
+
store.rollback(txid)
|
|
225
|
+
except CrowError as inner:
|
|
226
|
+
raise CrowError("partial_state", "Guia falhou e as skills da transação " + txid + " não puderam ser desfeitas (" +
|
|
227
|
+
str(inner) + "). Execute crow-skills recover; ele desfaz o guia e as skills ligadas.") from exc
|
|
228
|
+
raise
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def linked_guides(store, txid):
|
|
232
|
+
"""Committed guide transactions that completed the given skills transaction."""
|
|
233
|
+
return [t["id"] for t in store.transactions() if t.get("kind") == "guide" and t.get("linked") == txid and t["phase"] == "committed"]
|
|
234
|
+
|
|
235
|
+
|
|
116
236
|
def run(args):
|
|
117
237
|
if args.version:
|
|
118
238
|
return {"version": __version__}
|
|
@@ -121,18 +241,24 @@ def run(args):
|
|
|
121
241
|
home = checked(getattr(args, "home", Path.home()))
|
|
122
242
|
store = Store(home)
|
|
123
243
|
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")):
|
|
244
|
+
if getattr(args, "fetch", False) and (getattr(args, "dry_run", False) or args.command not in ("install", "update", "list", "info", "related", "guide")):
|
|
125
245
|
raise CrowError("usage", "--fetch requer um comando com fonte e não pode ser combinado com --dry-run.")
|
|
126
246
|
if args.command == "agents":
|
|
127
247
|
return {"agents": inventory(home), "note": "Detecção por PATH/diretórios não comprova carregamento da skill."}
|
|
248
|
+
if args.command == "guide":
|
|
249
|
+
return guide_command(args, home, store, interactive)
|
|
128
250
|
if args.command in ("status", "doctor"):
|
|
129
251
|
targets = scope(args, home, read=True)
|
|
130
252
|
rows = store.status(targets)
|
|
131
253
|
pending = [t["id"] for t in store.pending()]
|
|
132
|
-
|
|
254
|
+
global_view = not (getattr(args, "project", None) or getattr(args, "dest", None))
|
|
255
|
+
names = [n.strip() for n in getattr(args, "agents", "").split(",")] if getattr(args, "agents", None) else list(guide.MECHANISMS)
|
|
256
|
+
guides = guide.status(store, home, names) if global_view else []
|
|
257
|
+
result = {"installations": rows, "guides": guides, "pending_transactions": pending,
|
|
133
258
|
"activation_verified": False, "reload_hint": "Peça ao host para recarregar/listar skills; reinicie a sessão se necessário."}
|
|
134
259
|
if args.command == "doctor":
|
|
135
|
-
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows),
|
|
260
|
+
result.update(healthy=not pending and all(r["status"] not in ("modified", "missing", "unsafe") for r in rows + guides),
|
|
261
|
+
agents=inventory(home))
|
|
136
262
|
return result
|
|
137
263
|
if args.command in ("rollback", "recover"):
|
|
138
264
|
if any(getattr(args, k, None) for k in ("agents", "dest", "project", "global_scope")):
|
|
@@ -142,12 +268,28 @@ def run(args):
|
|
|
142
268
|
expected = ("prepared", "restoring") if args.command == "recover" else ("committed",)
|
|
143
269
|
if any(t["phase"] not in expected for t in transactions):
|
|
144
270
|
raise CrowError("invalid_transaction", "Transação incompatível com a operação.")
|
|
145
|
-
|
|
271
|
+
# install/update/uninstall chain a skills transaction and a guide transaction
|
|
272
|
+
# (journal field "linked"). Undo both together: the guide first on rollback;
|
|
273
|
+
# on recover, an interrupted guide also undoes the skills it completed.
|
|
274
|
+
follow = []
|
|
275
|
+
if args.command == "rollback":
|
|
276
|
+
before_ids = [g for txid in ids for g in linked_guides(store, txid)]
|
|
277
|
+
ids, transactions = before_ids + ids, [store.journal(t) for t in before_ids] + transactions
|
|
278
|
+
else:
|
|
279
|
+
follow = [t["linked"] for t in transactions if t.get("linked")
|
|
280
|
+
and store.journal(t["linked"])["phase"] == "committed"]
|
|
281
|
+
view = lambda t: {"id": t["id"], "kind": t.get("kind", "skills"),
|
|
282
|
+
"targets": [i["path"] for i in t["items"]] if t.get("kind") == "guide" else list(t["before"])}
|
|
283
|
+
summary = {"transactions": [view(t) for t in transactions]}
|
|
284
|
+
if follow:
|
|
285
|
+
summary["then_rollback"] = [view(store.journal(t)) for t in follow]
|
|
146
286
|
if transactions:
|
|
147
287
|
authorize(args, interactive, summary)
|
|
148
288
|
if not getattr(args, "dry_run", False):
|
|
149
289
|
for txid in ids:
|
|
150
290
|
store.rollback(txid, recovery=args.command == "recover")
|
|
291
|
+
for txid in follow:
|
|
292
|
+
store.rollback(txid)
|
|
151
293
|
return {**summary, "dry_run": getattr(args, "dry_run", False), "applied": not getattr(args, "dry_run", False)}
|
|
152
294
|
source = None
|
|
153
295
|
if args.command != "uninstall":
|
|
@@ -185,12 +327,15 @@ def run(args):
|
|
|
185
327
|
source.require(name)
|
|
186
328
|
names.extend(n for n in source.catalog[name]["next"] if n not in names)
|
|
187
329
|
plan = store.plan(args.command, names, targets, source, getattr(args, "replace", False), args.overwrite_modified)
|
|
330
|
+
guides, gplan = guide_sync(args, home, store, args.command, targets, plan, source)
|
|
188
331
|
summary = {"changes": plan["changes"], "revision": source.revision if source else None,
|
|
189
332
|
"relationships": {n: source.related(n) for n in names} if source else {},
|
|
190
|
-
"handoffs_are_optional": True, "activation_verified": False}
|
|
191
|
-
if plan["items"]:
|
|
333
|
+
"guides": guides, "handoffs_are_optional": True, "activation_verified": False}
|
|
334
|
+
if plan["items"] or (gplan and gplan["items"]):
|
|
192
335
|
authorize(args, interactive, summary)
|
|
193
|
-
txid = None
|
|
336
|
+
txid = None
|
|
337
|
+
if not getattr(args, "dry_run", False):
|
|
338
|
+
txid, guides["transaction"] = apply_chain(store, plan, gplan, home)
|
|
194
339
|
return {**summary, "transaction": txid, "dry_run": getattr(args, "dry_run", False),
|
|
195
340
|
"applied": not getattr(args, "dry_run", False),
|
|
196
341
|
"reload_hint": ("Somente preview; nada instalado." if getattr(args, "dry_run", False) else
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"""Managed, additive agent guidance in each CLI's native always-loaded location.
|
|
2
|
+
|
|
3
|
+
Only mechanisms proven with a sentinel in a fresh interactive session of the
|
|
4
|
+
CLI are supported, all of them at user scope and all additive: the CLI keeps
|
|
5
|
+
loading its existing base/shared prompt next to this guide. Shared prompts
|
|
6
|
+
(for example a ~/.claude/CLAUDE.md symlink) and their targets are never
|
|
7
|
+
written, and mechanisms that replace a base prompt are not used.
|
|
8
|
+
|
|
9
|
+
Ownership: a whole file that this CLI creates (claude, gemini, devin) or a
|
|
10
|
+
single marked block holding one top-level key in a config file (codex). The
|
|
11
|
+
rest of a shared config file is preserved byte for byte.
|
|
12
|
+
"""
|
|
13
|
+
from copy import deepcopy
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
import hashlib
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import re
|
|
19
|
+
import stat
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
try: # Python 3.11+; required only to verify config.toml edits.
|
|
23
|
+
import tomllib
|
|
24
|
+
except ImportError: # pragma: no cover - CI and supported hosts have 3.11+
|
|
25
|
+
tomllib = None
|
|
26
|
+
|
|
27
|
+
from .agents import destinations
|
|
28
|
+
from .source import CrowError, SHA
|
|
29
|
+
from .storage import atomic_json, checked, private_dir, read_json
|
|
30
|
+
|
|
31
|
+
HEADER = "<!-- Gerenciado por crow-skills guide. Não edite; use crow-skills guide install/uninstall. -->"
|
|
32
|
+
BEGIN = "# >>> crow-skills guide (gerenciado; use crow-skills guide uninstall) >>>"
|
|
33
|
+
END = "# <<< crow-skills guide <<<"
|
|
34
|
+
TOML_KEY = "developer_instructions"
|
|
35
|
+
MAX_CONFIG = 4 * 1024 * 1024
|
|
36
|
+
HEX = re.compile(r"[a-f0-9]{64}\Z")
|
|
37
|
+
PRESERVED = re.compile(r"[a-f0-9]{32}/[a-z]+\.bak\Z")
|
|
38
|
+
|
|
39
|
+
# path is relative to the user's home. All entries were verified on 2026-10-21
|
|
40
|
+
# with a sentinel loaded in a fresh session alongside the existing base prompt.
|
|
41
|
+
MECHANISMS = {
|
|
42
|
+
"claude": {"kind": "file", "path": ".claude/rules/crow-skills.md", "frontmatter": False,
|
|
43
|
+
"label": "Claude Code", "invoke": "/{name}",
|
|
44
|
+
"load": "use a ferramenta Skill com o nome da skill ou digite /nome"},
|
|
45
|
+
"codex": {"kind": "toml", "path": ".codex/config.toml", "label": "Codex CLI", "invoke": "${name}",
|
|
46
|
+
"load": "mencione $nome ou abra o SKILL.md dela pela lista de skills disponíveis"},
|
|
47
|
+
"gemini": {"kind": "file", "path": ".gemini/config/rules/crow-skills.md", "frontmatter": True,
|
|
48
|
+
"label": "Antigravity CLI, agy", "invoke": "/{name}",
|
|
49
|
+
"load": "use a ferramenta de skills do agy ou digite /nome"},
|
|
50
|
+
"devin": {"kind": "file", "path": ".devin/rules/crow-skills.md", "frontmatter": True,
|
|
51
|
+
"label": "Devin CLI", "invoke": "/devin:{name}",
|
|
52
|
+
"load": "use a ferramenta de skills do Devin ou digite /devin:nome"},
|
|
53
|
+
}
|
|
54
|
+
UNVERIFIED = {
|
|
55
|
+
"shared": "shared é um diretório de skills, não um CLI; não há onde gravar instruções.",
|
|
56
|
+
}
|
|
57
|
+
# Shared prompt files (often symlinks into ~/.config/uniflow) and their targets.
|
|
58
|
+
PROTECTED = (".claude/CLAUDE.md", ".codex/AGENTS.md", ".codex/AGENTS.override.md", ".gemini/GEMINI.md",
|
|
59
|
+
".config/devin/CLAUDE.md", ".config/devin/AGENTS.md", ".config/uniflow")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def sha(data):
|
|
63
|
+
if data is None:
|
|
64
|
+
return None
|
|
65
|
+
if isinstance(data, str):
|
|
66
|
+
data = data.encode("utf-8")
|
|
67
|
+
return hashlib.sha256(data).hexdigest()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def now():
|
|
71
|
+
return datetime.now(timezone.utc).isoformat()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def select(agents):
|
|
75
|
+
if not agents:
|
|
76
|
+
raise CrowError("usage", "Selecione --agents; não há instalação implícita em todos os agentes.")
|
|
77
|
+
names = agents.split(",") if isinstance(agents, str) else list(agents)
|
|
78
|
+
result = []
|
|
79
|
+
for name in (n.strip() for n in names):
|
|
80
|
+
if name in UNVERIFIED:
|
|
81
|
+
raise CrowError("unsupported_guide", "Sem mecanismo verificado para " + name + ": " + UNVERIFIED[name])
|
|
82
|
+
if name not in MECHANISMS:
|
|
83
|
+
raise CrowError("unsupported_guide", "Sem mecanismo nativo verificado para '" + name +
|
|
84
|
+
"'. Suportados: " + ",".join(MECHANISMS) + ".")
|
|
85
|
+
if name not in result:
|
|
86
|
+
result.append(name)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def protect(path, home):
|
|
91
|
+
"""Refuse symlinked components, shared prompts and anything inside their targets."""
|
|
92
|
+
path = checked(path)
|
|
93
|
+
for rel in PROTECTED:
|
|
94
|
+
guard = Path(os.path.abspath(home / rel))
|
|
95
|
+
guards = {guard}
|
|
96
|
+
if os.path.lexists(guard):
|
|
97
|
+
guards.add(Path(os.path.realpath(guard)))
|
|
98
|
+
for item in guards:
|
|
99
|
+
if path == item or path.is_relative_to(item):
|
|
100
|
+
raise CrowError("protected_path", "Arquivo de prompt compartilhado (ou alvo dele) não é gravado: " + str(path))
|
|
101
|
+
return path
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def read_regular(path):
|
|
105
|
+
if not os.path.lexists(path):
|
|
106
|
+
return None
|
|
107
|
+
info = path.lstat()
|
|
108
|
+
if not stat.S_ISREG(info.st_mode):
|
|
109
|
+
raise CrowError("unsafe_destination", "Destino do guia não é arquivo regular: " + str(path))
|
|
110
|
+
if info.st_nlink > 1:
|
|
111
|
+
raise CrowError("unsafe_destination", "Destino do guia tem hard links; recusado: " + str(path))
|
|
112
|
+
if info.st_size > MAX_CONFIG:
|
|
113
|
+
raise CrowError("unsafe_destination", "Destino do guia excede o limite de leitura segura.")
|
|
114
|
+
return path.read_bytes()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def write_atomic(path, data, mode):
|
|
118
|
+
temp = path.parent / ("." + path.name + ".crow-" + uuid.uuid4().hex)
|
|
119
|
+
try:
|
|
120
|
+
fd = os.open(temp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o600)
|
|
121
|
+
with os.fdopen(fd, "wb") as f:
|
|
122
|
+
f.write(data)
|
|
123
|
+
f.flush()
|
|
124
|
+
os.fsync(f.fileno())
|
|
125
|
+
os.chmod(temp, mode)
|
|
126
|
+
os.replace(temp, path)
|
|
127
|
+
fd = os.open(path.parent, os.O_RDONLY)
|
|
128
|
+
try:
|
|
129
|
+
os.fsync(fd)
|
|
130
|
+
finally:
|
|
131
|
+
os.close(fd)
|
|
132
|
+
finally:
|
|
133
|
+
if os.path.lexists(temp):
|
|
134
|
+
temp.unlink()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# --- content -----------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def clean(text):
|
|
140
|
+
return "".join(c for c in str(text) if c.isprintable()).replace("'''", "'").replace("-->", "->")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def managed_skills(agent, home, store):
|
|
144
|
+
"""crow-* packages this CLI manages for the agent (edited copies still count)."""
|
|
145
|
+
target = destinations(agent, home)[0]
|
|
146
|
+
return sorted(r["name"] for r in store.status([target], ()) if r["status"] in ("installed", "modified"))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def present_skills(agent, home, store, catalog):
|
|
150
|
+
return [n for n in managed_skills(agent, home, store) if n in catalog]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def render_body(agent, catalog, revision, skills):
|
|
154
|
+
mech = MECHANISMS[agent]
|
|
155
|
+
invoke = mech["invoke"].format
|
|
156
|
+
specialists = [n for n in skills if n != "crow-behaviour"]
|
|
157
|
+
example = " (por exemplo, " + invoke(name=specialists[0]) + ")" if specialists else ""
|
|
158
|
+
lines = [
|
|
159
|
+
"# Crow skills neste agente (" + mech["label"] + ")",
|
|
160
|
+
"",
|
|
161
|
+
"Gerado por crow-skills guide a partir do catálogo " + revision[:12] + ". Complementa as instruções do usuário e do projeto; havendo conflito, elas prevalecem.",
|
|
162
|
+
"",
|
|
163
|
+
"## crow-behaviour: quando aplicar",
|
|
164
|
+
"- Tarefa substantiva (várias etapas ou partes, exploração e decisões, mudanças em vários arquivos ou componentes, ou entrega que exige verificação): 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.",
|
|
165
|
+
"- 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 com várias etapas continua sendo tarefa substantiva.",
|
|
166
|
+
"- Conversa, pergunta factual curta ou ajuste trivial e local: responda direto, sem carregá-la.",
|
|
167
|
+
"",
|
|
168
|
+
"## Skills especialistas",
|
|
169
|
+
]
|
|
170
|
+
lines += [
|
|
171
|
+
"- Antes de executar, consulte a lista de skills disponíveis neste agente e carregue pelo nome a especialista cujo domínio corresponde à tarefa" + example + ". Se a tarefa cruzar domínios, combine as pertinentes; a crow-behaviour coordena, a especialista executa.",
|
|
172
|
+
"- Handoffs entre skills são sugestões opcionais, não dependências; não carregue a biblioteca inteira.",
|
|
173
|
+
"- Se uma skill não estiver disponível no host, diga isso e siga sem ela; nunca simule o conteúdo de uma skill que não carregou.",
|
|
174
|
+
"",
|
|
175
|
+
"## Crow skills gerenciadas neste agente",
|
|
176
|
+
]
|
|
177
|
+
for name in skills:
|
|
178
|
+
lines.append("- " + invoke(name=name) + " — " + clean(catalog[name].get("title") or catalog[name]["owns"]))
|
|
179
|
+
return "\n".join(lines) + "\n"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def managed_content(agent, body):
|
|
183
|
+
"""Exact bytes owned by this CLI: a whole file, or one marked TOML block."""
|
|
184
|
+
mech = MECHANISMS[agent]
|
|
185
|
+
if mech["kind"] == "toml":
|
|
186
|
+
if "'''" in body or any(not (c.isprintable() or c in "\n\t") for c in body):
|
|
187
|
+
raise CrowError("invalid_guide", "Conteúdo do guia não cabe numa string TOML literal.")
|
|
188
|
+
return BEGIN + "\n" + TOML_KEY + " = '''\n" + body + "'''\n" + END + "\n"
|
|
189
|
+
front = "---\ntrigger: always_on\n---\n\n" if mech["frontmatter"] else ""
|
|
190
|
+
return front + HEADER + "\n\n" + body
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# --- codex config.toml block ------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def parse_toml(text):
|
|
196
|
+
if tomllib is None:
|
|
197
|
+
raise CrowError("unsupported_platform", "Python 3.11+ (tomllib) é necessário para editar config.toml com verificação.")
|
|
198
|
+
try:
|
|
199
|
+
return tomllib.loads(text)
|
|
200
|
+
except tomllib.TOMLDecodeError as exc:
|
|
201
|
+
raise CrowError("invalid_config", "config.toml não é TOML válido; nada alterado.") from exc
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def find_block(text):
|
|
205
|
+
"""Return (start, end) offsets of the managed block, or None."""
|
|
206
|
+
begins = [m.start() for m in re.finditer(r"(?m)^" + re.escape(BEGIN) + r"\n", text)]
|
|
207
|
+
ends = [m.end() for m in re.finditer(r"(?m)^" + re.escape(END) + r"\n", text)]
|
|
208
|
+
if not begins and not ends:
|
|
209
|
+
return None
|
|
210
|
+
if len(begins) != 1 or len(ends) != 1 or ends[0] <= begins[0]:
|
|
211
|
+
raise CrowError("guide_corrupted", "Marcadores do bloco crow-skills em config.toml estão incompletos ou duplicados; revise manualmente.")
|
|
212
|
+
return begins[0], ends[0]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def decode(data):
|
|
216
|
+
try:
|
|
217
|
+
return data.decode("utf-8")
|
|
218
|
+
except UnicodeDecodeError as exc:
|
|
219
|
+
raise CrowError("invalid_config", "config.toml não é UTF-8; nada alterado.") from exc
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def toml_block(data):
|
|
223
|
+
if data is None:
|
|
224
|
+
return None
|
|
225
|
+
text = decode(data)
|
|
226
|
+
span = find_block(text)
|
|
227
|
+
return text[span[0]:span[1]] if span else None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def toml_compose(old_bytes, old_block, new_block, body=None):
|
|
231
|
+
"""Remove the existing managed block and prepend the new one; verify parse."""
|
|
232
|
+
text = decode(old_bytes) if old_bytes is not None else ""
|
|
233
|
+
if old_block is not None:
|
|
234
|
+
start, end = find_block(text)
|
|
235
|
+
text = text[:start] + text[end:]
|
|
236
|
+
base = parse_toml(text)
|
|
237
|
+
if TOML_KEY in base:
|
|
238
|
+
raise CrowError("unmanaged_conflict", "config.toml já define " + TOML_KEY + " fora do bloco gerenciado; remova ou mova antes. Nada alterado.")
|
|
239
|
+
result = (new_block or "") + text
|
|
240
|
+
parsed = parse_toml(result)
|
|
241
|
+
expected = dict(base)
|
|
242
|
+
if new_block is not None:
|
|
243
|
+
expected[TOML_KEY] = body if body is not None else parse_toml(new_block)[TOML_KEY]
|
|
244
|
+
if parsed != expected:
|
|
245
|
+
raise CrowError("invalid_config", "Edição do config.toml não preservou o restante do arquivo; nada alterado.")
|
|
246
|
+
return result.encode("utf-8")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# --- state ----------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def state_path(store):
|
|
252
|
+
return store.root / "guides" / "state.json"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def validate_state(data):
|
|
256
|
+
try:
|
|
257
|
+
if data["schema_version"] != 1 or not isinstance(data["guides"], dict):
|
|
258
|
+
raise ValueError("schema")
|
|
259
|
+
for agent, record in data["guides"].items():
|
|
260
|
+
if agent not in MECHANISMS or record["mechanism"] != MECHANISMS[agent]["kind"]:
|
|
261
|
+
raise ValueError("agent")
|
|
262
|
+
if (not isinstance(record["path"], str) or not HEX.fullmatch(record["sha256"]) or
|
|
263
|
+
not SHA.fullmatch(record["revision"]) or not isinstance(record["skills"], list) or
|
|
264
|
+
not all(isinstance(d, str) for d in record.get("created_dirs", [])) or
|
|
265
|
+
not isinstance(record.get("created_file", False), bool)):
|
|
266
|
+
raise ValueError("record")
|
|
267
|
+
preserved = record.get("preserved")
|
|
268
|
+
if preserved is not None and (not HEX.fullmatch(preserved["sha256"]) or
|
|
269
|
+
not PRESERVED.fullmatch(preserved.get("backup") or "")):
|
|
270
|
+
raise ValueError("preserved")
|
|
271
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
272
|
+
raise CrowError("invalid_state", "Estado de guias inválido; nenhuma alteração aplicada.") from exc
|
|
273
|
+
return data
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def load_state(store):
|
|
277
|
+
path = checked(state_path(store))
|
|
278
|
+
if not path.exists():
|
|
279
|
+
return {"schema_version": 1, "guides": {}}
|
|
280
|
+
return validate_state(read_json(path))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def validate_journal(data, txid):
|
|
284
|
+
try:
|
|
285
|
+
if data["id"] != txid or data["phase"] not in ("prepared", "restoring", "committed", "rolled_back"):
|
|
286
|
+
raise ValueError("phase")
|
|
287
|
+
validate_state(data["before"])
|
|
288
|
+
validate_state(data["after"])
|
|
289
|
+
if "linked" in data and not re.fullmatch(r"[a-f0-9]{32}", data["linked"] or ""):
|
|
290
|
+
raise ValueError("linked")
|
|
291
|
+
if not isinstance(data["items"], list) or not data["items"]:
|
|
292
|
+
raise ValueError("items")
|
|
293
|
+
for item in data["items"]:
|
|
294
|
+
if item["agent"] not in MECHANISMS or item["kind"] != MECHANISMS[item["agent"]]["kind"]:
|
|
295
|
+
raise ValueError("agent")
|
|
296
|
+
if str(checked(item["path"])) != item["path"]:
|
|
297
|
+
raise ValueError("path")
|
|
298
|
+
for key in ("old_sha", "new_sha"):
|
|
299
|
+
if item[key] is not None and not HEX.fullmatch(item[key]):
|
|
300
|
+
raise ValueError("sha")
|
|
301
|
+
for key in ("old_block", "new_block"):
|
|
302
|
+
if item[key] is not None and not isinstance(item[key], str):
|
|
303
|
+
raise ValueError("block")
|
|
304
|
+
if item["backup"] is not None and not re.fullmatch(r"[a-z]+\.bak", item["backup"]):
|
|
305
|
+
raise ValueError("backup")
|
|
306
|
+
item.setdefault("removed_dirs", [])
|
|
307
|
+
for key in ("created_dirs", "removed_dirs"):
|
|
308
|
+
if not isinstance(item[key], list) or not all(isinstance(d, str) and str(checked(d)) == d for d in item[key]):
|
|
309
|
+
raise ValueError("dirs")
|
|
310
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
311
|
+
raise CrowError("invalid_transaction", "Registro transacional de guia inconsistente; recuperação recusada.") from exc
|
|
312
|
+
return data
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --- plan / apply / restore -----------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
def current(agent, home):
|
|
318
|
+
"""(path, file bytes, managed bytes-or-text) for an agent; never follows links."""
|
|
319
|
+
mech = MECHANISMS[agent]
|
|
320
|
+
path = protect(home / mech["path"], home)
|
|
321
|
+
data = read_regular(path)
|
|
322
|
+
if mech["kind"] == "toml":
|
|
323
|
+
return path, data, toml_block(data)
|
|
324
|
+
return path, data, data.decode("utf-8", "replace") if data is not None else None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def plan(store, action, agents, home, source=None, replace=False, overwrite=False, skills_after=None):
|
|
328
|
+
"""Plan guide changes. action is "install"/"uninstall" or a {agent: action} map.
|
|
329
|
+
|
|
330
|
+
skills_after maps an agent to the managed skills it will have once a pending
|
|
331
|
+
skills transaction applies; without it the current installation is used.
|
|
332
|
+
"""
|
|
333
|
+
if store.pending():
|
|
334
|
+
raise CrowError("recovery_required", "Há uma transação interrompida. Execute recover antes de alterar instalações.")
|
|
335
|
+
actions = action if isinstance(action, dict) else {agent: action for agent in agents}
|
|
336
|
+
before = load_state(store)
|
|
337
|
+
after = deepcopy(before)
|
|
338
|
+
items, changes, payload, previews = [], [], {}, {}
|
|
339
|
+
for agent in agents:
|
|
340
|
+
action = actions[agent]
|
|
341
|
+
mech = MECHANISMS[agent]
|
|
342
|
+
path, data, managed = current(agent, home)
|
|
343
|
+
record = before["guides"].get(agent)
|
|
344
|
+
if record and record["path"] != str(path):
|
|
345
|
+
raise CrowError("invalid_state", "Guia registrado em outro caminho para " + agent + "; revise o estado.")
|
|
346
|
+
managed_sha = sha(managed)
|
|
347
|
+
if action == "uninstall":
|
|
348
|
+
if not record:
|
|
349
|
+
raise CrowError("not_managed", "Guia não gerenciado para " + agent + "; nada removido.")
|
|
350
|
+
if managed_sha != record["sha256"] and not overwrite:
|
|
351
|
+
raise CrowError("local_changes", "Guia de " + agent + " alterado/ausente. Revise; --overwrite-modified autoriza backup e remoção.")
|
|
352
|
+
new_managed, body, skills = None, None, None
|
|
353
|
+
original = preserved_original(store, record)
|
|
354
|
+
else:
|
|
355
|
+
if "crow-behaviour" not in source.catalog:
|
|
356
|
+
raise CrowError("invalid_catalog", "Catálogo sem crow-behaviour.")
|
|
357
|
+
if skills_after is not None and agent in skills_after:
|
|
358
|
+
skills = [n for n in skills_after[agent] if n in source.catalog]
|
|
359
|
+
else:
|
|
360
|
+
skills = present_skills(agent, home, store, source.catalog)
|
|
361
|
+
if "crow-behaviour" not in skills:
|
|
362
|
+
raise CrowError("guide_requires_skill", "crow-behaviour não está instalada para " + agent +
|
|
363
|
+
"; instale antes: crow-skills install crow-behaviour --agents " + agent + " --global --yes")
|
|
364
|
+
body = render_body(agent, source.catalog, source.revision, skills)
|
|
365
|
+
new_managed = managed_content(agent, body)
|
|
366
|
+
previews[agent] = new_managed
|
|
367
|
+
if record and managed_sha == sha(new_managed):
|
|
368
|
+
after["guides"][agent].update(revision=source.revision, skills=skills)
|
|
369
|
+
changes.append({"agent": agent, "path": str(path), "mechanism": mech["kind"], "action": "unchanged"})
|
|
370
|
+
continue
|
|
371
|
+
if record and managed_sha != record["sha256"] and not overwrite:
|
|
372
|
+
raise CrowError("local_changes", "Guia de " + agent + " alterado/ausente desde a instalação. Revise; --overwrite-modified autoriza backup e substituição.")
|
|
373
|
+
if not record and managed is not None and not replace:
|
|
374
|
+
raise CrowError("unmanaged_conflict", "Já existe conteúdo não gerenciado em " + str(path) + ". Revise; --replace autoriza backup e substituição.")
|
|
375
|
+
if mech["kind"] == "toml":
|
|
376
|
+
new_data = toml_compose(data, managed, new_managed, body)
|
|
377
|
+
if new_data == b"" and new_managed is None and record.get("created_file", False):
|
|
378
|
+
new_data = None # This CLI created the file and only our block is left: remove it.
|
|
379
|
+
elif new_managed is not None:
|
|
380
|
+
new_data = new_managed.encode("utf-8")
|
|
381
|
+
else:
|
|
382
|
+
new_data = original # Uninstall: put back what --replace displaced, or remove our file.
|
|
383
|
+
created = []
|
|
384
|
+
if new_data is not None:
|
|
385
|
+
parent = path.parent
|
|
386
|
+
while not parent.exists() and parent != home and home in parent.parents:
|
|
387
|
+
created.append(str(parent))
|
|
388
|
+
parent = parent.parent
|
|
389
|
+
mode = stat.S_IMODE(path.lstat().st_mode) if data is not None else (0o600 if mech["kind"] == "toml" else 0o644)
|
|
390
|
+
items.append({"agent": agent, "kind": mech["kind"], "path": str(path), "old_sha": sha(data), "new_sha": sha(new_data),
|
|
391
|
+
"old_block": managed if mech["kind"] == "toml" else None,
|
|
392
|
+
"new_block": new_managed if mech["kind"] == "toml" else None,
|
|
393
|
+
"mode": mode, "backup": None, "created_dirs": created,
|
|
394
|
+
"removed_dirs": record.get("created_dirs", []) if action == "uninstall" else []})
|
|
395
|
+
payload[agent] = (data, new_data)
|
|
396
|
+
if action == "uninstall":
|
|
397
|
+
after["guides"].pop(agent)
|
|
398
|
+
act = "remove"
|
|
399
|
+
else:
|
|
400
|
+
entry = {"path": str(path), "mechanism": mech["kind"], "sha256": sha(new_managed),
|
|
401
|
+
"revision": source.revision, "skills": skills, "installed_at": now(),
|
|
402
|
+
"created_dirs": created + (record.get("created_dirs", []) if record else [])}
|
|
403
|
+
if mech["kind"] == "toml":
|
|
404
|
+
entry["created_file"] = record.get("created_file", False) if record else data is None
|
|
405
|
+
if record and record.get("preserved"):
|
|
406
|
+
entry["preserved"] = record["preserved"]
|
|
407
|
+
elif not record and data is not None and mech["kind"] == "file":
|
|
408
|
+
entry["preserved"] = {"sha256": sha(data)} # backup id is set by apply()
|
|
409
|
+
after["guides"][agent] = entry
|
|
410
|
+
act = "replace" if managed is not None else "install"
|
|
411
|
+
changes.append({"agent": agent, "path": str(path), "mechanism": mech["kind"], "action": act,
|
|
412
|
+
"edits": "bloco " + TOML_KEY + " marcado" if mech["kind"] == "toml" else "arquivo próprio"})
|
|
413
|
+
return {"before": before, "after": after, "items": items, "payload": payload, "changes": changes, "previews": previews}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def apply(store, plan_data, home, linked=None):
|
|
417
|
+
"""Apply a guide plan; linked is the skills transaction this one completes."""
|
|
418
|
+
if not plan_data["items"]:
|
|
419
|
+
if plan_data["after"] != plan_data["before"]:
|
|
420
|
+
with store.lock():
|
|
421
|
+
if load_state(store) != plan_data["before"]:
|
|
422
|
+
raise CrowError("changed_during_plan", "O estado mudou após o preview; execute novamente.")
|
|
423
|
+
atomic_json(state_path(store), plan_data["after"])
|
|
424
|
+
return None
|
|
425
|
+
with store.lock():
|
|
426
|
+
if store.pending():
|
|
427
|
+
raise CrowError("recovery_required", "Recuperação pendente; nenhuma alteração aplicada.")
|
|
428
|
+
if load_state(store) != plan_data["before"]:
|
|
429
|
+
raise CrowError("changed_during_plan", "O estado mudou após o preview; execute novamente.")
|
|
430
|
+
for item in plan_data["items"]:
|
|
431
|
+
if sha(read_regular(protect(Path(item["path"]), home))) != item["old_sha"]:
|
|
432
|
+
raise CrowError("changed_during_plan", "Um destino mudou após o preview; execute novamente.")
|
|
433
|
+
for item in plan_data["items"]:
|
|
434
|
+
# A skills transaction applied after planning may have created parents
|
|
435
|
+
# (for example ~/.claude); this transaction does not own those.
|
|
436
|
+
existing = [d for d in item["created_dirs"] if os.path.lexists(d)]
|
|
437
|
+
if existing:
|
|
438
|
+
item["created_dirs"] = [d for d in item["created_dirs"] if d not in existing]
|
|
439
|
+
entry = plan_data["after"]["guides"].get(item["agent"])
|
|
440
|
+
if entry is not None:
|
|
441
|
+
entry["created_dirs"] = [d for d in entry["created_dirs"] if d not in existing]
|
|
442
|
+
txid = uuid.uuid4().hex
|
|
443
|
+
backups = private_dir(store.root / "guide-backups") / txid
|
|
444
|
+
backups = private_dir(backups)
|
|
445
|
+
for item in plan_data["items"]:
|
|
446
|
+
old, _ = plan_data["payload"][item["agent"]]
|
|
447
|
+
if old is not None:
|
|
448
|
+
item["backup"] = item["agent"] + ".bak"
|
|
449
|
+
write_atomic(backups / item["backup"], old, 0o600)
|
|
450
|
+
kept = plan_data["after"]["guides"].get(item["agent"], {}).get("preserved")
|
|
451
|
+
if kept is not None and "backup" not in kept:
|
|
452
|
+
kept["backup"] = txid + "/" + item["backup"]
|
|
453
|
+
data = {"id": txid, "kind": "guide", "phase": "prepared", "created_at": now(),
|
|
454
|
+
"before": plan_data["before"], "after": plan_data["after"], "items": plan_data["items"]}
|
|
455
|
+
if linked:
|
|
456
|
+
data["linked"] = linked
|
|
457
|
+
journal = store.root / "transactions" / (txid + ".json")
|
|
458
|
+
atomic_json(journal, data)
|
|
459
|
+
try:
|
|
460
|
+
for item in plan_data["items"]:
|
|
461
|
+
path = protect(Path(item["path"]), home)
|
|
462
|
+
if sha(read_regular(path)) != item["old_sha"]:
|
|
463
|
+
raise CrowError("changed_during_install", "Destino do guia alterado durante a instalação.")
|
|
464
|
+
_, new = plan_data["payload"][item["agent"]]
|
|
465
|
+
if new is None:
|
|
466
|
+
if os.path.lexists(path):
|
|
467
|
+
path.unlink()
|
|
468
|
+
remove_empty(item["removed_dirs"])
|
|
469
|
+
else:
|
|
470
|
+
for directory in reversed(item["created_dirs"]):
|
|
471
|
+
os.mkdir(directory, 0o755)
|
|
472
|
+
write_atomic(path, new, item["mode"])
|
|
473
|
+
atomic_json(state_path(store), plan_data["after"])
|
|
474
|
+
data["phase"] = "committed"
|
|
475
|
+
atomic_json(journal, data)
|
|
476
|
+
except (Exception, KeyboardInterrupt):
|
|
477
|
+
restore(store, data, home)
|
|
478
|
+
raise
|
|
479
|
+
return txid
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def restore(store, data, home):
|
|
483
|
+
"""Return every guide to its pre-transaction content, refusing over later edits."""
|
|
484
|
+
state = load_state(store)
|
|
485
|
+
if state != data["before"] and state != data["after"]:
|
|
486
|
+
raise CrowError("recovery_conflict", "Estado de guias mudou após a transação; recuperação recusada.")
|
|
487
|
+
backups = store.root / "guide-backups" / data["id"]
|
|
488
|
+
operations = []
|
|
489
|
+
for item in reversed(data["items"]):
|
|
490
|
+
path = protect(Path(item["path"]), home)
|
|
491
|
+
now_data = read_regular(path)
|
|
492
|
+
now_sha = sha(now_data)
|
|
493
|
+
if now_sha == item["old_sha"]:
|
|
494
|
+
continue
|
|
495
|
+
old = None
|
|
496
|
+
if item["old_sha"] is not None:
|
|
497
|
+
old = read_regular(checked(backups / item["backup"]))
|
|
498
|
+
if sha(old) != item["old_sha"]:
|
|
499
|
+
raise CrowError("recovery_conflict", "Backup do guia ausente ou alterado; recuperação recusada.")
|
|
500
|
+
if now_sha == item["new_sha"]:
|
|
501
|
+
operations.append((path, old, item))
|
|
502
|
+
elif item["kind"] == "toml" and now_data is not None and toml_block(now_data) == item["new_block"]:
|
|
503
|
+
# Codex rewrote other parts of config.toml: undo only our block.
|
|
504
|
+
operations.append((path, toml_compose(now_data, item["new_block"], item["old_block"]), item))
|
|
505
|
+
else:
|
|
506
|
+
raise CrowError("recovery_conflict", "Guia de " + item["agent"] + " mudou após a transação; recuperação não sobrescreverá alterações.")
|
|
507
|
+
data["phase"] = "restoring"
|
|
508
|
+
atomic_json(store.root / "transactions" / (data["id"] + ".json"), data)
|
|
509
|
+
for path, content, item in operations:
|
|
510
|
+
if content is None:
|
|
511
|
+
if os.path.lexists(path):
|
|
512
|
+
path.unlink()
|
|
513
|
+
else:
|
|
514
|
+
for directory in reversed(item["removed_dirs"]):
|
|
515
|
+
if not os.path.lexists(directory):
|
|
516
|
+
os.mkdir(directory, 0o755)
|
|
517
|
+
write_atomic(path, content, item["mode"])
|
|
518
|
+
for item in data["items"]:
|
|
519
|
+
remove_empty(item["created_dirs"])
|
|
520
|
+
atomic_json(state_path(store), data["before"])
|
|
521
|
+
data["phase"] = "rolled_back"
|
|
522
|
+
atomic_json(store.root / "transactions" / (data["id"] + ".json"), data)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def preserved_original(store, record):
|
|
526
|
+
"""Bytes that --replace displaced for this guide, verified against the state; None if nothing was displaced."""
|
|
527
|
+
kept = record.get("preserved")
|
|
528
|
+
if not kept:
|
|
529
|
+
return None
|
|
530
|
+
data = read_regular(checked(store.root / "guide-backups" / kept["backup"]))
|
|
531
|
+
if sha(data) != kept["sha256"]:
|
|
532
|
+
raise CrowError("recovery_conflict", "Backup do conteúdo original substituído por --replace está ausente ou alterado; nada removido.")
|
|
533
|
+
return data
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def remove_empty(directories):
|
|
537
|
+
"""Remove directories this CLI created, deepest first, only while empty."""
|
|
538
|
+
for directory in directories:
|
|
539
|
+
try:
|
|
540
|
+
os.rmdir(checked(directory))
|
|
541
|
+
except OSError:
|
|
542
|
+
pass # Not empty or already gone: never remove foreign content.
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def rollback(store, data, home, recovery=False):
|
|
546
|
+
if not recovery and load_state(store) != data["after"]:
|
|
547
|
+
raise CrowError("rollback_conflict", "Há mudanças posteriores nos guias; rollback antigo recusado.")
|
|
548
|
+
restore(store, data, home)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# --- status ---------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
def status(store, home, agents):
|
|
554
|
+
state = load_state(store)
|
|
555
|
+
rows = []
|
|
556
|
+
for agent in agents:
|
|
557
|
+
if agent not in MECHANISMS:
|
|
558
|
+
continue
|
|
559
|
+
mech = MECHANISMS[agent]
|
|
560
|
+
record = state["guides"].get(agent)
|
|
561
|
+
row = {"agent": agent, "path": str(home / mech["path"]), "mechanism": mech["kind"],
|
|
562
|
+
"revision": record["revision"] if record else None, "activation_verified": False}
|
|
563
|
+
try:
|
|
564
|
+
path, data, managed = current(agent, home)
|
|
565
|
+
parsed = parse_toml(decode(data)) if mech["kind"] == "toml" and data is not None else {}
|
|
566
|
+
if record is None:
|
|
567
|
+
row["status"] = "unmanaged" if managed is not None or TOML_KEY in parsed else "absent"
|
|
568
|
+
elif managed is None:
|
|
569
|
+
row["status"] = "missing"
|
|
570
|
+
elif sha(managed) != record["sha256"]:
|
|
571
|
+
row["status"] = "modified"
|
|
572
|
+
elif mech["kind"] == "toml" and parsed.get(TOML_KEY) != parse_toml(managed).get(TOML_KEY):
|
|
573
|
+
row["status"] = "modified"
|
|
574
|
+
row["note"] = "Bloco gerenciado não está mais no nível superior do config.toml."
|
|
575
|
+
elif managed_skills(agent, home, store) != record["skills"]:
|
|
576
|
+
row["status"] = "outdated"
|
|
577
|
+
row["note"] = "Skills instaladas mudaram; execute crow-skills guide install para regenerar."
|
|
578
|
+
else:
|
|
579
|
+
row["status"] = "installed"
|
|
580
|
+
except CrowError as exc:
|
|
581
|
+
row["status"] = "unsafe"
|
|
582
|
+
row["note"] = str(exc)
|
|
583
|
+
rows.append(row)
|
|
584
|
+
return rows
|
|
585
|
+
|
package/crow_skills/storage.py
CHANGED
|
@@ -154,6 +154,9 @@ class Store:
|
|
|
154
154
|
if not re.fullmatch(r"[a-f0-9]{32}", txid):
|
|
155
155
|
raise CrowError("invalid_transaction", "ID de transação inválido.")
|
|
156
156
|
data = read_json(self.root / "transactions" / (txid + ".json"))
|
|
157
|
+
if isinstance(data, dict) and data.get("kind") == "guide":
|
|
158
|
+
from .guide import validate_journal
|
|
159
|
+
return validate_journal(data, txid)
|
|
157
160
|
try:
|
|
158
161
|
if data["id"] != txid or data["phase"] not in ("prepared", "restoring", "committed", "rolled_back"):
|
|
159
162
|
raise ValueError("phase")
|
|
@@ -356,6 +359,9 @@ class Store:
|
|
|
356
359
|
raise CrowError("invalid_transaction", "Estado da transação incompatível com a operação.")
|
|
357
360
|
if not recovery and self.pending():
|
|
358
361
|
raise CrowError("recovery_required", "Resolva transações interrompidas primeiro.")
|
|
362
|
+
if data.get("kind") == "guide":
|
|
363
|
+
from .guide import rollback
|
|
364
|
+
return rollback(self, data, self.home, recovery)
|
|
359
365
|
if not recovery:
|
|
360
366
|
for target, state in data["after"].items():
|
|
361
367
|
if self.state(target) != state:
|
package/docs/cli-security.md
CHANGED
|
@@ -22,6 +22,8 @@ Checklist aplicável: autorização por escopo e flags (sim); validação de nom
|
|
|
22
22
|
| Rollback antigo apagando versão nova | Comparação de estado posterior e inventário antes de restaurar; teste de transação subsequente |
|
|
23
23
|
| Dois instaladores concorrentes | Lock POSIX por perfil; segundo processo real deve retornar busy |
|
|
24
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
|
+
| Guia de agente sobrescrevendo prompt compartilhado ou config alheia | Só mecanismos aditivos verificados; recusa de symlinks, hard links, prompts compartilhados e seus alvos; bloco TOML marcado validado por parse, chave alheia nunca editada; testes de symlink, hard link, chave estrangeira e remoção byte a byte |
|
|
26
|
+
| Install/update aplicando o guia de forma parcial ou sobre conteúdo alheio | Guia planejado e conferido antes de gravar skills; recusa falha o comando inteiro sem estado parcial; falha do guia desfaz as skills; transações encadeadas (`linked`) com rollback/recover conjuntos; `--no-guide` explícito; mesmas proteções de Uniflow/symlink/hard link; testes de recusa, falha, interrupção e rollback |
|
|
25
27
|
| 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
28
|
|
|
27
29
|
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.
|
package/docs/cli.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# crow-skills CLI 0.
|
|
1
|
+
# crow-skills CLI 0.3.0
|
|
2
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
|
|
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 o agente, não executa scripts das skills e não muda modelos, prompts base ou permissões. Instruções de uso das skills (o guia do agente, abaixo) são gravadas pelo comando `guide` e, por padrão, por `install`/`update`/`uninstall` em `--global` quando a crow-behaviour fica instalada para um agente com mecanismo verificado; `--no-guide` desliga isso.
|
|
4
4
|
|
|
5
5
|
## Executar
|
|
6
6
|
|
|
@@ -17,6 +17,8 @@ O launcher do checkout pode ser referenciado por um symlink em um diretório já
|
|
|
17
17
|
|
|
18
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
19
|
|
|
20
|
+
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`.
|
|
21
|
+
|
|
20
22
|
Depois de a versão estar publicada e a conta estar autenticada com acesso:
|
|
21
23
|
|
|
22
24
|
```sh
|
|
@@ -29,7 +31,7 @@ Para testar/distribuir o arquivo local sem registry:
|
|
|
29
31
|
|
|
30
32
|
```sh
|
|
31
33
|
npm pack
|
|
32
|
-
npm install -g ./andespindola-crow-skills-0.
|
|
34
|
+
npm install -g ./andespindola-crow-skills-0.3.0.tgz --ignore-scripts
|
|
33
35
|
```
|
|
34
36
|
|
|
35
37
|
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.
|
|
@@ -49,6 +51,8 @@ crow-skills install crow-debug --dest /caminho/do/host/skills --yes
|
|
|
49
51
|
|
|
50
52
|
`--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
53
|
|
|
54
|
+
Em `--global`, quando o resultado deixa a crow-behaviour instalada para claude, codex, gemini ou devin, o mesmo comando aplica ou regenera o guia desse agente (veja [Instruções para o agente](#instruções-para-o-agente-guide)); `--no-guide` desliga esse passo.
|
|
55
|
+
|
|
52
56
|
`--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
57
|
|
|
54
58
|
## Complementaridade
|
|
@@ -96,6 +100,7 @@ crow-skills recover --yes
|
|
|
96
100
|
- 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
101
|
- 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
102
|
- `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.
|
|
103
|
+
- `install`/`update`/`uninstall` que também alteram o guia gravam duas transações encadeadas: skills e depois guia, com o journal do guia apontando a de skills (`linked`). As duas IDs saem no resultado (`transaction` e `guides.transaction`). Se o guia falhar, as skills dessa execução são desfeitas antes do erro. `rollback` da transação de skills desfaz primeiro o guia ligado; `rollback` da transação de guia desfaz só o guia. `recover` de um guia interrompido também desfaz as skills ligadas (`then_rollback` no preview). Uma interrupção exatamente entre as duas deixa as skills aplicadas e o guia `outdated`/`absent`, sem journal pendente: repita o comando ou faça rollback da transação de skills.
|
|
99
104
|
- `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
105
|
- 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
106
|
- Uninstall só remove nomes gerenciados. Sem nomes exige `--all` explícito. Não elimina skills de terceiros nem diretórios alheios.
|
|
@@ -133,6 +138,43 @@ Erros retornam `ok:false` e `error.code/message`. Saídas reais incluem alteraç
|
|
|
133
138
|
|
|
134
139
|
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
140
|
|
|
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.
|
|
141
|
+
`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 base é alterado para forçar uso; o guia abaixo, aplicado por padrão em `--global` e desligável com `--no-guide`, é aditivo.
|
|
137
142
|
|
|
138
143
|
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).
|
|
144
|
+
|
|
145
|
+
## Instruções para o agente (guide)
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
crow-skills install crow-behaviour --agents claude,codex,gemini,devin --global --yes # aplica o guia junto
|
|
149
|
+
crow-skills install crow-behaviour --agents claude --global --no-guide --yes # só a skill
|
|
150
|
+
crow-skills guide install --agents claude,codex,gemini,devin --global --dry-run
|
|
151
|
+
crow-skills guide install --agents claude,codex,gemini,devin --global --yes
|
|
152
|
+
crow-skills guide status
|
|
153
|
+
crow-skills guide uninstall --agents codex --global --yes
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`guide` grava, no mecanismo nativo e sempre carregado de cada CLI, instruções curtas em pt-BR para o agente usar as skills instaladas: carregar a crow-behaviour por iniciativa própria antes de tarefas substantivas (várias etapas ou partes, decisões, vários arquivos, verificação), responder direto em conversa ou ajuste trivial, consultar a lista de skills disponíveis no agente (crow ou não) e carregar a especialista pertinente pelo nome. O conteúdo é gerado do catálogo e das skills gerenciadas pelo CLI naquele agente, com a forma de invocação dele. Exige a crow-behaviour instalada para o agente.
|
|
157
|
+
|
|
158
|
+
Aplicação automática (padrão em `install`, `update` e `uninstall` com `--global`, desligada por `--no-guide`):
|
|
159
|
+
|
|
160
|
+
- O guia é planejado junto com as skills, a partir do conjunto de skills que o agente terá depois do comando, e conferido antes de gravar qualquer coisa. Install/update que deixam a crow-behaviour instalada aplicam o guia ou o regeneram; mudança de outra skill gerenciada regenera um guia já instalado (também quando a mudança chega pelo diretório compartilhado `~/.agents/skills`, que é o do codex). Uninstall da crow-behaviour remove o guia, devolvendo o arquivo que `--replace` havia deslocado; uninstall de outra skill regenera o guia instalado e nunca cria um novo.
|
|
161
|
+
- Recusa do guia (arquivo não gerenciado sem `--replace`, guia editado/ausente sem `--overwrite-modified`, chave `developer_instructions` alheia, symlink, hard link, prompt compartilhado ou Uniflow) falha o comando inteiro, com código do guia e sugestão de `--replace`, `--overwrite-modified` ou `--no-guide`, sem gravar skills nem guia. Em install/update, `--replace` e `--overwrite-modified` valem para skills e guia.
|
|
162
|
+
- `shared`, `--project` e `--dest` não têm mecanismo: o comando segue e informa em `guides.skipped`. Sem Python 3.11+ (tomllib), o guia do codex também é pulado e informado.
|
|
163
|
+
- `--dry-run` inclui `guides.changes` e `guides.preview` (conteúdo que seria gravado). A saída JSON traz `guides` com `changes`, `skipped`, `revision` e `transaction`.
|
|
164
|
+
|
|
165
|
+
| Agente | Mecanismo (usuário) | Propriedade |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| claude | `~/.claude/rules/crow-skills.md` | arquivo próprio |
|
|
168
|
+
| codex | chave `developer_instructions` no topo de `~/.codex/config.toml` | bloco marcado; o restante do arquivo fica intacto |
|
|
169
|
+
| gemini (Antigravity `agy`) | `~/.gemini/config/rules/crow-skills.md`, `trigger: always_on` | arquivo próprio |
|
|
170
|
+
| devin | `~/.devin/rules/crow-skills.md`, `trigger: always_on` | arquivo próprio |
|
|
171
|
+
|
|
172
|
+
Cada mecanismo foi verificado em 2026-10-21 (Claude Code 2.1.304, Codex CLI 0.163.0, agy 1.4.3, Devin CLI 3000.11.92) com uma sentinela carregada numa sessão nova junto do prompt base já existente: os mecanismos são aditivos. Versões futuras dos CLIs podem mudar isso; confira abrindo uma sessão nova. O comando não inicia agentes e retorna `activation_verified:false`.
|
|
173
|
+
|
|
174
|
+
- Só `--global` é suportado. No comando `guide`, `--project`, `--dest` e `shared` são recusados com `unsupported_guide`, porque não há mecanismo verificado.
|
|
175
|
+
- Nunca grava em symlinks, em arquivos com hard links, em prompts compartilhados (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.gemini/GEMINI.md`, `~/.config/devin/CLAUDE.md`), em `~/.config/uniflow/` ou nos alvos desses links. Não usa mecanismos que substituem o prompt base (como `model_instructions_file` ou `AGENTS.override.md`).
|
|
176
|
+
- No Codex, uma chave `developer_instructions` definida fora do bloco é recusada sempre, mesmo com `--replace`. A edição é validada por parse TOML (Python 3.11+): o restante do arquivo precisa continuar idêntico. Uninstall devolve o arquivo byte a byte.
|
|
177
|
+
- Arquivo existente não gerenciado exige `--replace`; guia editado ou ausente exige `--overwrite-modified`. Ambos guardam backup privado em `~/.local/state/crow-skills/guide-backups/`. O uninstall devolve o arquivo que `--replace` substituiu (conferido pelo SHA-256 do backup), em vez de apagá-lo, e mantém um `config.toml` que já existia, mesmo vazio.
|
|
178
|
+
- Transações usam o mesmo journal, lock, `rollback` e `recover` das skills. Se o Codex reescrever outras partes do `config.toml` depois, o rollback desfaz só o bloco.
|
|
179
|
+
- `status`/`doctor` mostram `guides`: `installed`, `outdated` (as skills gerenciadas mudaram com `--no-guide` ou fora do CLI; rode `guide install` ou `update` de novo), `modified`, `missing`, `unmanaged`, `absent` ou `unsafe`. Doctor fica `healthy:false` para `modified`, `missing` e `unsafe`.
|
|
180
|
+
- Diretórios de regras criados pelo comando são removidos no uninstall se estiverem vazios.
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"README.md": {
|
|
4
4
|
"executable": false,
|
|
5
5
|
"kind": "file",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "b354e9907359550f40b94ab52fb3a38162a7cdf950d33de419f51e82e8b1423e"
|
|
7
7
|
},
|
|
8
8
|
"bin/npm-crow-skills.cjs": {
|
|
9
9
|
"executable": true,
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"catalog.json": {
|
|
14
14
|
"executable": false,
|
|
15
15
|
"kind": "file",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "07c01ffc61cab5c6c7f3c2374110705dc8746c8267bb5a0c25f88c8b601f2242"
|
|
17
17
|
},
|
|
18
18
|
"crow_skills/__init__.py": {
|
|
19
19
|
"executable": false,
|
|
20
20
|
"kind": "file",
|
|
21
|
-
"sha256": "
|
|
21
|
+
"sha256": "f81792d3fad0ea3fdeb96616d1df41e0f9cf811e1aee8e48946ffc7077a8469b"
|
|
22
22
|
},
|
|
23
23
|
"crow_skills/__main__.py": {
|
|
24
24
|
"executable": false,
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
"crow_skills/cli.py": {
|
|
34
34
|
"executable": false,
|
|
35
35
|
"kind": "file",
|
|
36
|
-
"sha256": "
|
|
36
|
+
"sha256": "9aa14347630210decb6a4e4fc58f1910dbeb53ec6907d1cf819939b7419e4df4"
|
|
37
|
+
},
|
|
38
|
+
"crow_skills/guide.py": {
|
|
39
|
+
"executable": false,
|
|
40
|
+
"kind": "file",
|
|
41
|
+
"sha256": "65b93fff61c496f7d3e1b3b32eadb008ba3a9cfd543a4db53f73becdb4ceec6f"
|
|
37
42
|
},
|
|
38
43
|
"crow_skills/npm_entry.py": {
|
|
39
44
|
"executable": false,
|
|
@@ -48,22 +53,22 @@
|
|
|
48
53
|
"crow_skills/storage.py": {
|
|
49
54
|
"executable": false,
|
|
50
55
|
"kind": "file",
|
|
51
|
-
"sha256": "
|
|
56
|
+
"sha256": "2c3823f783c1da2f971860e6207a5146275e105fab74a7aa54e57033cd3993f0"
|
|
52
57
|
},
|
|
53
58
|
"docs/cli-security.md": {
|
|
54
59
|
"executable": false,
|
|
55
60
|
"kind": "file",
|
|
56
|
-
"sha256": "
|
|
61
|
+
"sha256": "33eebc401bf86443569f011b9987d7e954a9f5b5ade6bb2e18e26055b9ed74f2"
|
|
57
62
|
},
|
|
58
63
|
"docs/cli.md": {
|
|
59
64
|
"executable": false,
|
|
60
65
|
"kind": "file",
|
|
61
|
-
"sha256": "
|
|
66
|
+
"sha256": "a0af861933004eac9581215fd5a27312e0127cf1e21f9e8bac12b957200c5fdb"
|
|
62
67
|
},
|
|
63
68
|
"package.json": {
|
|
64
69
|
"executable": false,
|
|
65
70
|
"kind": "file",
|
|
66
|
-
"sha256": "
|
|
71
|
+
"sha256": "4985632e9d45f238022af574b5533c0df90e509276decac44f90a3e37c7973ec"
|
|
67
72
|
},
|
|
68
73
|
"skills/crow-accessibility/SKILL.md": {
|
|
69
74
|
"executable": false,
|
|
@@ -93,7 +98,7 @@
|
|
|
93
98
|
"skills/crow-behaviour/SKILL.md": {
|
|
94
99
|
"executable": false,
|
|
95
100
|
"kind": "file",
|
|
96
|
-
"sha256": "
|
|
101
|
+
"sha256": "d58086484a9e914f4e1b4dbded62834fd9105a5df10076888852b8ed5ed5d303"
|
|
97
102
|
},
|
|
98
103
|
"skills/crow-behaviour/agents/openai.yaml": {
|
|
99
104
|
"executable": false,
|
|
@@ -601,7 +606,7 @@
|
|
|
601
606
|
"sha256": "a04946ef7637e0d3fbaedc650b4ddcba9c8f7c71708e837c7e44779d7d6263ca"
|
|
602
607
|
}
|
|
603
608
|
},
|
|
604
|
-
"revision": "
|
|
609
|
+
"revision": "d5b45e0d80d58a203ff86a9f4211fbd63a57d655",
|
|
605
610
|
"schema_version": 1,
|
|
606
|
-
"version": "0.
|
|
611
|
+
"version": "0.3.0"
|
|
607
612
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crow-behaviour
|
|
3
|
-
description: "
|
|
3
|
+
description: "Use PROATIVAMENTE, sem o usuário pedir, sempre que a tarefa for substantiva — multi-etapa, com exploração/decisões, mexendo em vários arquivos ou componentes, ou exigindo verificação antes de entregar (implementar uma feature, refatorar, depurar, auditar, revisar, migrar, pesquisar a fundo). Ao ativar: decomponha o trabalho, delegue com as capacidades reais do runtime, verifique de forma adversarial e sintetize evidências. NÃO ative em conversa, explicação curta ou ajuste trivial/local — nesses casos responda direto. Não simule agentes nem altere permissões."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Atuação coordenada
|