@andespindola/crow-skills 0.3.0 → 0.5.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.
@@ -17,12 +17,24 @@ Quando a solução ainda for desconhecida, separe perguntas falsificáveis e res
17
17
 
18
18
  Não imponha consenso: dois agentes podem compartilhar a mesma suposição errada. Sem independência real, rotule como perspectivas da mesma execução.
19
19
 
20
+ ## Painel com juízes
21
+
22
+ Para decisões de design com várias saídas plausíveis: N propostas independentes (mesmo problema, sem ver umas às outras); avaliadores paralelos pontuam cada uma pelos critérios do usuário, com justificativa ligada a evidência; sintetize a vencedora incorporando o melhor das demais. Os juízes recebem as propostas e os critérios, não a preferência do coordenador. Empate ou divergência forte entre juízes é sinal para buscar a evidência que decide, não para votar de novo.
23
+
20
24
  ## Pesquisa com triangulação
21
25
 
22
- Separe eixos complementares, não só variações de palavras-chave. Exija fontes rastreáveis, data/versão, observações e inferências separadas. Na síntese, identifique fontes derivadas entre si e conflitos de versão; não conte cópias da mesma origem como confirmações independentes.
26
+ Separe eixos complementares, não só variações de palavras-chave, e execute-os em paralelo. Exija fontes rastreáveis, data/versão, observações e inferências separadas. Na síntese, identifique fontes derivadas entre si e conflitos de versão; não conte cópias da mesma origem como confirmações independentes.
23
27
 
24
28
  Verifique afirmações centrais contra a fonte primária quando disponível. Falta de acesso é desconhecido, não refutação. Não preencha o relatório com fontes irrelevantes para aparentar cobertura.
25
29
 
30
+ ## Varredura por vários eixos
31
+
32
+ Em revisão e auditoria, como em pesquisa, divida a busca por dimensões (por exemplo, corretude, segurança, desempenho, compatibilidade, dados, operação) e execute cada eixo em paralelo, com seu próprio executor. Cada achado segue para a verificação adversária; achados duplicados entre eixos são deduplicados numa barreira antes da verificação.
33
+
34
+ ## Busca até secar
35
+
36
+ Em descoberta, auditoria ou revisão, repita rodadas com ângulos novos até K rodadas consecutivas sem item novo (K = 2 por padrão; maior quando o custo de um item perdido é alto). Registre no ledger K, o número de rodadas e o motivo de parada (secou, limite explícito ou capacidade ausente). Isto é diferente de "Corrigir até passar": lá o loop termina quando um check passa; aqui termina quando a descoberta para de render itens.
37
+
26
38
  ## Fan-out por item: auditoria ou migração
27
39
 
28
40
  Faça inventário do conjunto-alvo e estabeleça uma unidade verificável por item/lote. Preserve identidade de cada item e resultado inclusive em falha, cancelamento ou retorno vazio. Não filtre falhas silenciosamente para produzir uma taxa de sucesso melhor.
@@ -39,8 +51,14 @@ Depois de uma falha, formule causa e correção, aplique o menor ajuste autoriza
39
51
 
40
52
  O executor entrega o artefato e sua evidência; o revisor recebe requisitos e artefato e tenta refutar o aceite. Revise fronteiras: entradas inválidas, permissões, concorrência, compatibilidade, recuperação ou fidelidade de conteúdo, conforme o domínio real.
41
53
 
54
+ Por padrão, salvo trabalho trivial ou já verificado nesta revisão, submeta cada achado ou alegação a vários verificadores independentes instruídos a refutar, cada um com uma lente distinta (corretude, segurança, desempenho, reprodução). O achado cai se a maioria refutar, salvo evidência reproduzível (teste, comando, trecho de fonte primária) que resista às refutações; nesse caso ele fica, e a divergência é registrada com o motivo. Verificadores que leram a mesma fonte derivada contam como uma só confirmação.
55
+
42
56
  O principal valida achados contestados e reconcilia revisão com testes. Não peça ao revisor apenas que confirme que o executor está certo. Quando não houver segundo agente, faça revisão sequencial sem afirmar independência, e prefira testes/medidas reproduzíveis para reduzir viés.
43
57
 
58
+ ## Crítico de completude
59
+
60
+ Antes de encerrar, uma unidade separada (outro executor, ou uma passada sem o contexto da produção quando não houver outro) recebe o pedido original, os critérios e a entrega, e lista apenas o que falta ou não está demonstrado. Itens novos voltam para o plano; uma resposta vazia conta para a parada. Não peça ao crítico que aprove a entrega.
61
+
44
62
  ## Critério de conclusão
45
63
 
46
64
  Antes de encerrar, confronte pedido e entrega:
@@ -51,6 +69,8 @@ Antes de encerrar, confronte pedido e entrega:
51
69
  | O que foi realmente executado? | Saída observada, alvo/revisão e condição do teste. |
52
70
  | As partes funcionam juntas? | Verificação de interfaces, não só aprovação isolada. |
53
71
  | Há alegação sem base ou conflito escondido? | Fonte, teste ou rótulo de desconhecido; motivo da resolução. |
54
- | O trabalho pode continuar sem repetir tudo? | Estado remanescente, artefatos válidos e próxima ação. |
72
+ | A descoberta secou? | Rodadas sem item novo (K), crítico de completude sem lacuna nova, ou limite explícito registrado. |
73
+ | O que ficou de fora? | Itens cortados ou não verificados, com motivo; nenhum corte silencioso. |
74
+ | O trabalho pode continuar sem repetir tudo? | Ledger com estado remanescente, artefatos válidos e próxima ação. |
55
75
 
56
76
  A resposta final prioriza o resultado e limitações que mudam a decisão do usuário. A coordenação fica em segundo plano; só detalhe a metodologia quando solicitado ou necessário para interpretar a confiança.
@@ -0,0 +1,279 @@
1
+ #!/usr/bin/env python3
2
+ """Run ledger for crow-behaviour: record units and print the chat summary.
3
+
4
+ Standard library only; runs on any host that can execute python3. The
5
+ coordinator is the single writer of a ledger; workers report back and the
6
+ coordinator records the transition.
7
+
8
+ crow_ledger.py start --title "..." --host claude [--phase id=Título ...] [--session ID] [--global]
9
+ crow_ledger.py phase ID --run RUN [--title T] [--status running|done|...]
10
+ crow_ledger.py unit ID --run RUN [--title T] [--phase P] [--owner O] [--status S] [--progress 0..1] [--summary TXT]
11
+ crow_ledger.py finish --run RUN [--status done|failed|cancelled]
12
+ crow_ledger.py show [--run RUN] # tree + ASCII bars for the chat
13
+ crow_ledger.py path [--run RUN] # where the ledger lives
14
+
15
+ start prints "<run-id> <path>"; pass that run id as --run (or $CROW_RUN) on
16
+ every later call. Without it the helper only proceeds when exactly one
17
+ running ledger matches the working directory, so parallel sessions in the
18
+ same clone never write to each other's run.
19
+
20
+ Location: $CROW_LEDGER (a file) when set; with --global, or when the working
21
+ directory is not writable, ~/.crow/runs/<run-id>.json; else
22
+ <cwd>/.crow/runs/<run-id>.json, with .crow/.gitignore = "*" so the directory
23
+ never shows up in Git. Lookups walk from the working directory up to the
24
+ filesystem root, so a run started at the repository root is still found from
25
+ a subdirectory.
26
+ """
27
+ import argparse
28
+ from datetime import datetime, timezone
29
+ import json
30
+ import os
31
+ from pathlib import Path
32
+ import re
33
+ import sys
34
+ import tempfile
35
+
36
+ SCHEMA = "crow-run/1"
37
+ UNIT_STATUSES = ("pending", "running", "done", "failed", "skipped")
38
+ RUN_STATUSES = ("running", "done", "failed", "cancelled")
39
+
40
+
41
+ def now():
42
+ return datetime.now(timezone.utc).isoformat(timespec="seconds")
43
+
44
+
45
+ def slug(text):
46
+ value = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
47
+ return value[:40] or "run"
48
+
49
+
50
+ def run_dirs():
51
+ """<dir>/.crow/runs from the working directory up to the root, then ~/.crow/runs."""
52
+ cwd = Path.cwd()
53
+ dirs = [d / ".crow" / "runs" for d in (cwd, *cwd.parents)]
54
+ dirs.append(Path.home() / ".crow" / "runs")
55
+ result = []
56
+ for d in dirs:
57
+ if d not in result:
58
+ result.append(d)
59
+ return result
60
+
61
+
62
+ def under(cwd, base):
63
+ """True when cwd is base or one of its subdirectories."""
64
+ base = str(base).rstrip("/") or "/"
65
+ return cwd == base or cwd.startswith(base.rstrip("/") + "/")
66
+
67
+
68
+ def writable_dir(global_only=False):
69
+ home_runs = Path.home() / ".crow" / "runs"
70
+ choices = [home_runs] if global_only else [Path.cwd() / ".crow" / "runs", home_runs]
71
+ for index, directory in enumerate(choices):
72
+ try:
73
+ directory.mkdir(parents=True, exist_ok=True)
74
+ if index == 0 and directory != home_runs:
75
+ ignore = directory.parent / ".gitignore"
76
+ if not ignore.exists():
77
+ ignore.write_text("*\n", encoding="utf-8")
78
+ probe = tempfile.NamedTemporaryFile(dir=directory, delete=True)
79
+ probe.close()
80
+ return directory
81
+ except OSError:
82
+ continue
83
+ raise SystemExit("crow_ledger: nenhum diretório gravável para o ledger.")
84
+
85
+
86
+ def save(path, data):
87
+ data["updatedAt"] = now()
88
+ path = Path(path)
89
+ path.parent.mkdir(parents=True, exist_ok=True)
90
+ fd, temp = tempfile.mkstemp(dir=path.parent, prefix="." + path.name + ".")
91
+ try:
92
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
93
+ json.dump(data, handle, ensure_ascii=False, indent=2)
94
+ handle.write("\n")
95
+ os.replace(temp, path)
96
+ finally:
97
+ if os.path.exists(temp):
98
+ os.unlink(temp)
99
+
100
+
101
+ def candidates():
102
+ if os.environ.get("CROW_LEDGER"):
103
+ return [Path(os.environ["CROW_LEDGER"]).expanduser()]
104
+ files = []
105
+ for directory in run_dirs():
106
+ if directory.is_dir():
107
+ files += directory.glob("*.json")
108
+ return files
109
+
110
+
111
+ def locate(run_id=None, write=False):
112
+ """Find the ledger to read or update.
113
+
114
+ With a run id (--run or $CROW_RUN) the match is exact. Without one, only
115
+ ledgers whose cwd is the working directory or one of its ancestors are
116
+ considered, and a write is refused when more than one of them is running:
117
+ guessing would put this session's units into another session's run.
118
+ """
119
+ run_id = run_id or os.environ.get("CROW_RUN") or None
120
+ pinned = bool(os.environ.get("CROW_LEDGER"))
121
+ cwd = str(Path.cwd())
122
+ found = []
123
+ for path in candidates():
124
+ try:
125
+ data = json.loads(path.read_text(encoding="utf-8"))
126
+ mtime = path.stat().st_mtime
127
+ except (OSError, ValueError):
128
+ continue
129
+ if not isinstance(data, dict) or data.get("schema") != SCHEMA:
130
+ continue
131
+ if run_id and data.get("id") != run_id:
132
+ continue
133
+ if not run_id and not pinned and isinstance(data.get("cwd"), str) and not under(cwd, data["cwd"]):
134
+ continue
135
+ found.append(((data.get("status") == "running", mtime), path, data))
136
+ if not found:
137
+ raise SystemExit("crow_ledger: nenhuma execução encontrada" + (" com id " + run_id if run_id else "") +
138
+ "; use start primeiro.")
139
+ running = [item for item in found if item[2].get("status") == "running"]
140
+ if not run_id and not pinned and len(running) > 1 and write:
141
+ ids = ", ".join(sorted(str(item[2].get("id")) for item in running))
142
+ raise SystemExit("crow_ledger: mais de uma execução em andamento neste diretório (" + ids +
143
+ "); passe --run <id> (o id impresso por start).")
144
+ best = max(found, key=lambda item: item[0])
145
+ return best[1], best[2]
146
+
147
+
148
+ def bar(fraction, width=20):
149
+ filled = int(round(max(0.0, min(1.0, fraction)) * width))
150
+ return "[" + "#" * filled + "-" * (width - filled) + "]"
151
+
152
+
153
+ def clean(text, limit=120):
154
+ text = " ".join("".join(c if c.isprintable() else " " for c in str(text or "")).split())
155
+ return text if len(text) <= limit else text[: limit - 3] + "..."
156
+
157
+
158
+ def render(data):
159
+ units = data.get("units", [])
160
+ counted = [u for u in units if u.get("status") != "skipped"]
161
+ done = sum(u.get("status") == "done" for u in counted)
162
+ overall = sum(1.0 if u.get("status") in ("done", "skipped") else float(u.get("progress") or 0) for u in units) / len(units) if units else 0.0
163
+ icons = {"done": "x", "running": ">", "failed": "!", "pending": " ", "skipped": "-"}
164
+ lines = ["crow > " + clean(data.get("title")) + " [" + data.get("status", "running") + "]",
165
+ bar(overall) + " " + str(int(overall * 100)) + "% " + str(done) + "/" + str(len(counted)) + " unidades"]
166
+ phases = data.get("phases", []) or []
167
+ known = [p.get("id") for p in phases]
168
+ groups = [(p.get("title") or p.get("id"), [u for u in units if u.get("phase") == p.get("id")]) for p in phases]
169
+ loose = [u for u in units if u.get("phase") not in known]
170
+ if loose:
171
+ groups.append(("(sem fase)", loose))
172
+ for title, members in groups:
173
+ finished = sum(u.get("status") in ("done", "skipped") for u in members)
174
+ lines.append("- " + clean(title) + " " + str(finished) + "/" + str(len(members)))
175
+ for unit in members:
176
+ progress = 1.0 if unit.get("status") in ("done", "skipped") else float(unit.get("progress") or 0)
177
+ owner = " @" + clean(unit.get("owner"), 30) if unit.get("owner") else ""
178
+ lines.append(" [" + icons.get(unit.get("status"), "?") + "] " + clean(unit.get("title") or unit.get("id"), 70) +
179
+ " " + bar(progress, 10) + " " + str(int(progress * 100)) + "%" + owner)
180
+ if unit.get("summary") and unit.get("status") in ("done", "failed"):
181
+ lines.append(" " + clean(unit["summary"]))
182
+ return "\n".join(lines)
183
+
184
+
185
+ def main(argv=None):
186
+ p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
187
+ sub = p.add_subparsers(dest="command", required=True)
188
+ start = sub.add_parser("start")
189
+ start.add_argument("--title", required=True)
190
+ start.add_argument("--host", default="other",
191
+ help="claude, codex, gemini (Gemini CLI), agy (Antigravity; nunca gemini), devin ou other")
192
+ start.add_argument("--phase", action="append", default=[], help="id=Título; repetir na ordem")
193
+ start.add_argument("--session", help="ID da sessão do host, se conhecido (tokens exatos e status line da sessão certa)")
194
+ start.add_argument("--global", dest="global_only", action="store_true",
195
+ help="grava em ~/.crow/runs (tarefa somente leitura no diretório ou clone compartilhado)")
196
+ phase = sub.add_parser("phase")
197
+ phase.add_argument("id")
198
+ phase.add_argument("--title")
199
+ phase.add_argument("--status", choices=UNIT_STATUSES)
200
+ unit = sub.add_parser("unit")
201
+ unit.add_argument("id")
202
+ unit.add_argument("--title")
203
+ unit.add_argument("--phase")
204
+ unit.add_argument("--owner")
205
+ unit.add_argument("--status", choices=UNIT_STATUSES)
206
+ unit.add_argument("--progress", type=float)
207
+ unit.add_argument("--summary")
208
+ finish = sub.add_parser("finish")
209
+ finish.add_argument("--status", choices=RUN_STATUSES[1:], default="done")
210
+ for name in ("show", "path"):
211
+ child = sub.add_parser(name)
212
+ child.add_argument("--run")
213
+ for child in (phase, unit, finish):
214
+ child.add_argument("--run")
215
+ args = p.parse_args(argv)
216
+
217
+ if args.command == "start":
218
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
219
+ run_id = stamp + "-" + slug(args.title)
220
+ path = Path(os.environ["CROW_LEDGER"]).expanduser() if os.environ.get("CROW_LEDGER") else writable_dir(args.global_only) / (run_id + ".json")
221
+ phases = []
222
+ for item in args.phase:
223
+ pid, _, title = item.partition("=")
224
+ phases.append({"id": pid.strip(), "title": (title or pid).strip(), "status": "pending"})
225
+ data = {"schema": SCHEMA, "id": run_id, "title": args.title, "host": args.host.lower(), "cwd": str(Path.cwd()),
226
+ "status": "running", "startedAt": now(), "endedAt": None, "phases": phases, "units": []}
227
+ if args.session:
228
+ data["session"] = {"host": args.host.lower(), "id": args.session}
229
+ save(path, data)
230
+ print(run_id + " " + str(path))
231
+ return 0
232
+ path, data = locate(getattr(args, "run", None), write=args.command in ("phase", "unit", "finish"))
233
+ if args.command == "path":
234
+ print(path)
235
+ return 0
236
+ if args.command == "show":
237
+ print(render(data))
238
+ return 0
239
+ if args.command == "phase":
240
+ phases = data.setdefault("phases", [])
241
+ entry = next((x for x in phases if x.get("id") == args.id), None)
242
+ if entry is None:
243
+ entry = {"id": args.id, "title": args.title or args.id, "status": "pending"}
244
+ phases.append(entry)
245
+ if args.title:
246
+ entry["title"] = args.title
247
+ if args.status:
248
+ entry["status"] = args.status
249
+ elif args.command == "unit":
250
+ units = data.setdefault("units", [])
251
+ entry = next((x for x in units if x.get("id") == args.id), None)
252
+ if entry is None:
253
+ entry = {"id": args.id, "title": args.title or args.id, "owner": "", "status": "pending", "progress": 0.0,
254
+ "startedAt": None, "endedAt": None, "summary": ""}
255
+ units.append(entry)
256
+ for field in ("title", "phase", "owner", "summary"):
257
+ if getattr(args, field) is not None:
258
+ entry[field] = getattr(args, field)
259
+ if args.progress is not None:
260
+ entry["progress"] = max(0.0, min(1.0, args.progress))
261
+ if args.status:
262
+ entry["status"] = args.status
263
+ if args.status == "running" and not entry.get("startedAt"):
264
+ entry["startedAt"] = now()
265
+ if args.status in ("done", "failed", "skipped"):
266
+ entry["endedAt"] = now()
267
+ if not entry.get("startedAt"):
268
+ entry["startedAt"] = entry["endedAt"]
269
+ if args.status in ("done", "skipped"):
270
+ entry["progress"] = 1.0
271
+ elif args.command == "finish":
272
+ data["status"] = args.status
273
+ data["endedAt"] = now()
274
+ save(path, data)
275
+ return 0
276
+
277
+
278
+ if __name__ == "__main__":
279
+ sys.exit(main())