@andespindola/crow-skills 0.38.0 → 0.38.1
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/crow_skills/__init__.py +1 -1
- package/crow_skills/bots.py +49 -1
- package/crow_skills/cli.py +70 -2
- package/package.json +1 -1
- package/release.json +7 -7
- package/skills/crow-behaviour/SKILL.md +1 -0
package/crow_skills/__init__.py
CHANGED
package/crow_skills/bots.py
CHANGED
|
@@ -179,6 +179,8 @@ def spawn(home, role, brief, cli=None, model=None, owner=None, cwd=None):
|
|
|
179
179
|
"model": model,
|
|
180
180
|
"owner": owner,
|
|
181
181
|
"cwd": cwd or os.getcwd(),
|
|
182
|
+
"briefs": [brief],
|
|
183
|
+
"findings": [],
|
|
182
184
|
"state": "working",
|
|
183
185
|
"spawnedAt": broadcast.now_iso(),
|
|
184
186
|
"lastAssignedAt": broadcast.now_iso(),
|
|
@@ -207,7 +209,13 @@ def assign(home, bot_id, brief, owner=None):
|
|
|
207
209
|
pid, error = _launch(home, record, brief, resume=True)
|
|
208
210
|
if error:
|
|
209
211
|
raise RuntimeError(error)
|
|
210
|
-
|
|
212
|
+
# Keep every brief, do not overwrite. The knowledge a bot holds came from
|
|
213
|
+
# the base agent, so the briefs ARE the record of what it was taught; an
|
|
214
|
+
# assign that replaced the previous one erased run 1 when run 2 arrived,
|
|
215
|
+
# and with it the only trace of why the bot knows what it knows.
|
|
216
|
+
history = list(record.get("briefs") or ([record["brief"]] if record.get("brief") else []))
|
|
217
|
+
history.append(brief)
|
|
218
|
+
record.update(pid=pid, state="working", brief=brief, briefs=history[-20:],
|
|
211
219
|
lastAssignedAt=broadcast.now_iso(),
|
|
212
220
|
runs=int(record.get("runs") or 0) + 1)
|
|
213
221
|
if owner:
|
|
@@ -216,6 +224,46 @@ def assign(home, bot_id, brief, owner=None):
|
|
|
216
224
|
return record
|
|
217
225
|
|
|
218
226
|
|
|
227
|
+
def learned(home, bot_id, note):
|
|
228
|
+
"""Record what a bot REPORTED BACK, as the base agent read it.
|
|
229
|
+
|
|
230
|
+
The bot keeps its own context and never touches the vault; only the base
|
|
231
|
+
agent does. So the findings have to be written down HERE by whoever read
|
|
232
|
+
the report, or they vanish with the session and the next bot of the same
|
|
233
|
+
role starts from nothing — paying again for knowledge already bought.
|
|
234
|
+
|
|
235
|
+
This is the local, machine-level record. Promoting it to the vault, so the
|
|
236
|
+
knowledge survives the machine and reaches a second one, stays the base
|
|
237
|
+
agent's job: the durable note is written through tesus, not from here.
|
|
238
|
+
"""
|
|
239
|
+
note = (note or "").strip()
|
|
240
|
+
if not note:
|
|
241
|
+
raise ValueError("um aprendizado vazio não é aprendizado")
|
|
242
|
+
data = _load(home)
|
|
243
|
+
record = next((b for b in data["bots"] if b.get("id") == bot_id), None)
|
|
244
|
+
if not record:
|
|
245
|
+
raise ValueError("bot desconhecido: " + str(bot_id))
|
|
246
|
+
findings = list(record.get("findings") or [])
|
|
247
|
+
findings.append({"at": broadcast.now_iso(), "note": note})
|
|
248
|
+
record["findings"] = findings[-50:]
|
|
249
|
+
_save(home, data)
|
|
250
|
+
return record
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def knowledge(home, role):
|
|
254
|
+
"""Everything known about a role, across its bots: the briefs that taught
|
|
255
|
+
them and the findings they returned. This is what seeds the NEXT bot of the
|
|
256
|
+
same role instead of briefing it from zero."""
|
|
257
|
+
role = (role or "").strip().lower()
|
|
258
|
+
briefs, findings = [], []
|
|
259
|
+
for b in _load(home)["bots"]:
|
|
260
|
+
if (b.get("role") or "").strip().lower() != role:
|
|
261
|
+
continue
|
|
262
|
+
briefs.extend(b.get("briefs") or ([b["brief"]] if b.get("brief") else []))
|
|
263
|
+
findings.extend(b.get("findings") or [])
|
|
264
|
+
return {"role": role, "briefs": briefs, "findings": findings}
|
|
265
|
+
|
|
266
|
+
|
|
219
267
|
def release(home, bot_id):
|
|
220
268
|
"""Hand a borrowed bot back to its owner."""
|
|
221
269
|
data = _load(home)
|
package/crow_skills/cli.py
CHANGED
|
@@ -211,7 +211,7 @@ def parser():
|
|
|
211
211
|
child.add_argument("--follow", action="store_true", help="tail: transmitir eventos novos em JSONL até Ctrl-C")
|
|
212
212
|
child = command_parser(sub, "bot", [common])
|
|
213
213
|
child.add_argument("action", nargs="?", default="list",
|
|
214
|
-
choices=("list", "spawn", "assign", "release", "stop", "prune"))
|
|
214
|
+
choices=("list", "spawn", "assign", "release", "stop", "prune", "learned", "knowledge"))
|
|
215
215
|
child.add_argument("target", nargs="?", help="assign/release/stop: id do bot")
|
|
216
216
|
child.add_argument("text", nargs="*", help="assign: o novo briefing")
|
|
217
217
|
child.add_argument("--role", help="spawn: a especialidade do bot (é o que permite reaproveitá-lo)")
|
|
@@ -219,6 +219,7 @@ def parser():
|
|
|
219
219
|
child.add_argument("--cli", default="auto", help="spawn: claude, codex, gemini, devin ou auto (primeiro disponível)")
|
|
220
220
|
child.add_argument("--model", help="spawn: modelo para esta unidade")
|
|
221
221
|
child.add_argument("--lendable-only", action="store_true", help="list: só os ociosos que outra sessão pode pedir emprestado")
|
|
222
|
+
child.add_argument("--role-of", dest="role_of", help="knowledge: o papel cujo conhecimento acumulado você quer ler")
|
|
222
223
|
child = command_parser(sub, "broadcast", [common])
|
|
223
224
|
child.add_argument("action", nargs="?", default="status", choices=("status", "list", "claim", "release", "watch", "say", "reply", "inbox",
|
|
224
225
|
"thread", "sent", "ack", "feed"))
|
|
@@ -582,6 +583,32 @@ def ancestors(limit=16):
|
|
|
582
583
|
return pids
|
|
583
584
|
|
|
584
585
|
|
|
586
|
+
#: The human owner speaking from outside any agent session (desktop app, bare
|
|
587
|
+
#: shell). Deliberately NOT one of hook.HOSTS: it is not an agent and must not
|
|
588
|
+
#: be mistaken for one in the roster or the feed.
|
|
589
|
+
OWNER_HOST = "owner"
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def sender_owner(args, home, cwd):
|
|
593
|
+
"""Who is SENDING. Same resolution as broadcast_owner, but a caller with no
|
|
594
|
+
live-agent ancestry is the owner's own console rather than an error.
|
|
595
|
+
|
|
596
|
+
A desktop app is reparented to launchd, so it matches no session and
|
|
597
|
+
broadcast_owner returns None; `say` then dereferenced it into
|
|
598
|
+
"AttributeError: 'NoneType' object has no attribute 'get'" and the owner
|
|
599
|
+
could never speak on the channel from outside a terminal. The identity is
|
|
600
|
+
deterministic so agents can address a reply back to it.
|
|
601
|
+
|
|
602
|
+
Scoped to sending ON PURPOSE: claims, inbox, sent and ack keep requiring a
|
|
603
|
+
real session, because attributing those to a console would be wrong.
|
|
604
|
+
"""
|
|
605
|
+
found = broadcast_owner(args, home, cwd)
|
|
606
|
+
if found:
|
|
607
|
+
return found
|
|
608
|
+
return {"key": broadcast.session_key(OWNER_HOST, OWNER_HOST, None), "host": OWNER_HOST,
|
|
609
|
+
"session": None, "detected": "owner"}
|
|
610
|
+
|
|
611
|
+
|
|
585
612
|
def broadcast_owner(args, home, cwd):
|
|
586
613
|
"""The session that runs this command: --session/--host, else the live session whose CLI pid is an ancestor."""
|
|
587
614
|
live = broadcast.live_sessions(home)
|
|
@@ -710,6 +737,14 @@ def bot_command(args, home):
|
|
|
710
737
|
raise CrowError("usage", str(exc))
|
|
711
738
|
out = {"bot": record, "text": "bot " + record["id"] + " (" + record["role"] + ") iniciado em " +
|
|
712
739
|
str(record.get("cli")) + ", pid " + str(record.get("pid")) + " — você não ficou bloqueado"}
|
|
740
|
+
known = bots.knowledge(home, getattr(args, "role", "") or "")
|
|
741
|
+
if known["briefs"] or known["findings"]:
|
|
742
|
+
# Informs, does not inject: silently prepending it would inflate the
|
|
743
|
+
# brief, which is the very context cost this is meant to protect.
|
|
744
|
+
out["knowledge"] = {"briefs": len(known["briefs"]), "findings": len(known["findings"])}
|
|
745
|
+
out["text"] += ("\nJá há conhecimento deste papel: " + str(len(known["briefs"])) + " briefing(s) e " +
|
|
746
|
+
str(len(known["findings"])) + " achado(s). Leia com `bot knowledge --role-of " +
|
|
747
|
+
(getattr(args, "role", "") or "") + "` e inclua no briefing o que valer.")
|
|
713
748
|
if free:
|
|
714
749
|
# Not a refusal: the decision is the caller's, but spending a new
|
|
715
750
|
# process while an idle specialist sits there is the waste this
|
|
@@ -736,6 +771,29 @@ def bot_command(args, home):
|
|
|
736
771
|
raise CrowError("usage", str(exc))
|
|
737
772
|
return {"bot": record, "text": "bot " + record["id"] + " " +
|
|
738
773
|
("devolvido ao dono" if action == "release" else "encerrado")}
|
|
774
|
+
if action == "learned":
|
|
775
|
+
# The bot keeps its own context and never writes to the vault; whoever
|
|
776
|
+
# read its report records the finding here, or it dies with the session.
|
|
777
|
+
note = " ".join(getattr(args, "text", []) or []).strip()
|
|
778
|
+
if not getattr(args, "target", None) or not note:
|
|
779
|
+
raise CrowError("usage", "learned precisa do id do bot e do que ele reportou.")
|
|
780
|
+
try:
|
|
781
|
+
record = bots.learned(home, args.target, note)
|
|
782
|
+
except ValueError as exc:
|
|
783
|
+
raise CrowError("usage", str(exc))
|
|
784
|
+
return {"bot": record,
|
|
785
|
+
"text": "registrado no bot " + record["id"] + " (" + str(len(record.get("findings") or [])) +
|
|
786
|
+
" achado(s)). Isto é local: promova ao vault o que for durável."}
|
|
787
|
+
if action == "knowledge":
|
|
788
|
+
role = getattr(args, "role_of", None) or getattr(args, "target", None)
|
|
789
|
+
if not role:
|
|
790
|
+
raise CrowError("usage", "knowledge precisa do papel: --role-of <papel>.")
|
|
791
|
+
data = bots.knowledge(home, role)
|
|
792
|
+
n_b, n_f = len(data["briefs"]), len(data["findings"])
|
|
793
|
+
return {**data,
|
|
794
|
+
"text": ("papel \"" + data["role"] + "\": " + str(n_b) + " briefing(s) e " + str(n_f) +
|
|
795
|
+
" achado(s) acumulados" if n_b or n_f else
|
|
796
|
+
"papel \"" + data["role"] + "\": nada acumulado ainda")}
|
|
739
797
|
if action == "prune":
|
|
740
798
|
return bots.prune(home)
|
|
741
799
|
raise CrowError("usage", "ação de bot desconhecida: " + str(action))
|
|
@@ -819,7 +877,9 @@ def broadcast_command(args, home):
|
|
|
819
877
|
raise CrowError("not_found", "Nenhuma mensagem endereçada a esta sessão com esse(s) id(s) (expirada, inexistente, ou enviada por você).")
|
|
820
878
|
return {"schema": broadcast.MESSAGES_SCHEMA, "messages": acked,
|
|
821
879
|
"text": "leitura confirmada: " + ", ".join(str(m.get("id")) for m in acked)}
|
|
822
|
-
|
|
880
|
+
# Sending is the one path where a caller outside any agent session is
|
|
881
|
+
# legitimate: it is the owner talking from their own console.
|
|
882
|
+
owner = sender_owner(args, home, cwd) if action in ("say", "reply") else broadcast_owner(args, home, cwd)
|
|
823
883
|
if action in ("say", "reply"):
|
|
824
884
|
reply_to = getattr(args, "reply_to", None)
|
|
825
885
|
targets = list(args.targets)
|
|
@@ -833,6 +893,14 @@ def broadcast_command(args, home):
|
|
|
833
893
|
ttl = broadcast.parse_ttl(args.ttl) if args.ttl else None
|
|
834
894
|
except ValueError:
|
|
835
895
|
raise CrowError("usage", "--ttl inválido: use 30m, 2h, 90s (máximo 24h).") from None
|
|
896
|
+
if not isinstance(owner, dict) or not owner.get("key"):
|
|
897
|
+
# send_message dereferences the sender inside a dict literal, so a
|
|
898
|
+
# missing one surfaced as "AttributeError: 'NoneType' object has no
|
|
899
|
+
# attribute 'get'". Guarded HERE, not in broadcast.py: that module
|
|
900
|
+
# deliberately carries no dependency on source/CrowError, and adding
|
|
901
|
+
# one closes an import cycle through hook.py (measured: 52 tests).
|
|
902
|
+
raise CrowError("usage", "Remetente desconhecido: informe --session/--host "
|
|
903
|
+
"ou envie a partir de uma sessão viva.")
|
|
836
904
|
try:
|
|
837
905
|
record = broadcast.send_message(home, " ".join(targets), owner, to=args.to, ttl=ttl, cwd=cwd,
|
|
838
906
|
reply_to=reply_to, wake=not getattr(args, "no_wake", False))
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"crow_skills/__init__.py": {
|
|
29
29
|
"executable": false,
|
|
30
30
|
"kind": "file",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "5c2d124b8f54ae50df371268045f54ba0e08e8cdb27f2141773bfb80fba661b1"
|
|
32
32
|
},
|
|
33
33
|
"crow_skills/__main__.py": {
|
|
34
34
|
"executable": false,
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"crow_skills/bots.py": {
|
|
49
49
|
"executable": false,
|
|
50
50
|
"kind": "file",
|
|
51
|
-
"sha256": "
|
|
51
|
+
"sha256": "b6119c6974d06152b3462c67368b0fd81d4be389419838eebf3c5007e421a10d"
|
|
52
52
|
},
|
|
53
53
|
"crow_skills/broadcast.py": {
|
|
54
54
|
"executable": false,
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"crow_skills/cli.py": {
|
|
59
59
|
"executable": false,
|
|
60
60
|
"kind": "file",
|
|
61
|
-
"sha256": "
|
|
61
|
+
"sha256": "7fdd9af628596e8b7abc111b9277cc6498f5a68e6bdab9421e9aec35c0374ab3"
|
|
62
62
|
},
|
|
63
63
|
"crow_skills/events.py": {
|
|
64
64
|
"executable": false,
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"package.json": {
|
|
149
149
|
"executable": false,
|
|
150
150
|
"kind": "file",
|
|
151
|
-
"sha256": "
|
|
151
|
+
"sha256": "64e8d706972b11b51c6decc284fa11abb151d87e977e43723eb3849d1f409d87"
|
|
152
152
|
},
|
|
153
153
|
"skills/crow-accessibility/SKILL.md": {
|
|
154
154
|
"executable": false,
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"skills/crow-behaviour/SKILL.md": {
|
|
214
214
|
"executable": false,
|
|
215
215
|
"kind": "file",
|
|
216
|
-
"sha256": "
|
|
216
|
+
"sha256": "e7f2cdb1acf214e78be8f8b7c27c013e591478df7ac99addd3e0891cbc0d13b9"
|
|
217
217
|
},
|
|
218
218
|
"skills/crow-behaviour/agents/openai.yaml": {
|
|
219
219
|
"executable": false,
|
|
@@ -971,7 +971,7 @@
|
|
|
971
971
|
"sha256": "160c814085dfdbe93e0166f2a773345dd1436f07a63dbf58d447f3f7674dc759"
|
|
972
972
|
}
|
|
973
973
|
},
|
|
974
|
-
"revision": "
|
|
974
|
+
"revision": "b86c3ef157a305468fa37ce18f84cca40dc1b11d",
|
|
975
975
|
"schema_version": 1,
|
|
976
|
-
"version": "0.38.
|
|
976
|
+
"version": "0.38.1"
|
|
977
977
|
}
|
|
@@ -62,6 +62,7 @@ Quando `crow-model-level` estiver instalada, o nível de cada unidade delegada
|
|
|
62
62
|
- **O braçal é delegado por padrão, não por escolha sua.** Uma unidade é braçal quando você consegue especificar o resultado e aceitá-lo pela evidência, sem ver o caminho: a edição que você já decidiu, a varredura com critério fechado, a reescrita mecânica, a coleta de evidência, rodar um comando e relatar o que saiu, levantar como uma coisa funciona hoje. Se é preciso decidir no meio do caminho, não é braçal — é desenho, e é seu. Executar você mesmo uma unidade braçal porque "o contexto já está carregado" é a falha característica desta skill: troca o seu contexto, que é o recurso escasso e não se recupera, por trabalho que qualquer executor faria, e o briefing que você economizou custa menos do que o contexto que você gastou.
|
|
63
63
|
- Ao delegar execução, descubra quem é o micro-agent em vez de presumir: `crow_micro.py probe` — com `codex` disponível e autenticado, os micro-agents são do codex; sem ele, são os do próprio CLI. Ao micro-agent vai só o braçal já especificado; mapear, planejar, desenhar e verificar no fim continuam seus. Leia [micro-agents](references/micro-agents.md).
|
|
64
64
|
- **Micro-agent ou bot autônomo: decida pela natureza da demanda, não por hábito.** Resultado que você especifica e aceita pela evidência, dentro do turno, é **micro-agent** — efêmero, você verifica e descarta. Demanda que persiste além do turno, dispara por evento e precisa de estado próprio entre execuções é **bot autônomo** — um processo que sobrevive à sessão, com memória externa própria e gatilho; ele não lembra, relê o que gravou e reconstrói o contexto a cada disparo. Micro-agent é o default; bot só quando a persistência ou o gatilho de evento é a essência da demanda, não um "seria bom ter". Bot e micro-agent usam igualmente o crow-broadcast e a mesma fronteira de autonomia desta skill; o criador continua responsável por ambos, verifica o resultado e age quando qualquer um fica ocioso.
|
|
65
|
+
- **O conhecimento do bot é seu para guardar, não dele.** A memória do bot é apartada e perecível: ela morre com a sessão e envelhece quando ele fica ocioso. O que ele sabe veio de você, no briefing — ele é consumidor do conhecimento, nunca o guardião. Então, antes de criar um bot de um papel que já existiu, **leia o que já foi acumulado** (`crow-skills bot knowledge --role-of <papel>`) e inclua no briefing o que valer, em vez de ensinar de novo o que já foi pago. Quando o bot reporta, **registre o que ele devolveu** (`crow-skills bot learned <id> "<o que apurou>"`): o retorno não registrado é conhecimento que ninguém mais tem. Esse registro é local e mecânico; **promover ao durável é seu**, pela memória do dono, porque o bot não toca o vault — só você toca. Destile, não acumule: nota que cresce a cada briefing recria, em outro lugar, o custo de contexto que a delegação existe para cortar. E trate o retorno como alegação a julgar antes de virar registro, não como verdade.
|
|
65
66
|
- Leia [adaptação ao runtime](references/orchestration.md) ao escolher workflow, subagentes ou execução sequencial. Não presuma nomes de ferramentas, paralelismo ou suporte a isolamento. Se o host exigir opt-in explícito do usuário para orquestração programável, esta skill não é esse opt-in: peça o opt-in da ferramenta, nunca permissão para aplicar esta skill, e sem ele siga com subagentes ou sequencialmente.
|
|
66
67
|
|
|
67
68
|
### 2. Planeje para produzir e verificar
|