@andespindola/crow-skills 0.19.0 → 0.20.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/crow_skills/__init__.py +1 -1
- package/crow_skills/autoupdate.py +73 -0
- package/crow_skills/broadcast.py +23 -7
- package/crow_skills/machine.py +18 -2
- package/package.json +1 -1
- package/release.json +7 -7
package/crow_skills/__init__.py
CHANGED
|
@@ -319,6 +319,69 @@ def broken_projections(agent, home, store, rows):
|
|
|
319
319
|
return [{"name": n, "status": "uniflow_link_modified", "fix": broken_fix(n)} for n in sorted(names)]
|
|
320
320
|
|
|
321
321
|
|
|
322
|
+
|
|
323
|
+
def catalog_gap(agent, home, source, rows, projected, broken):
|
|
324
|
+
"""What this agent is missing from the catalog, and whether upgrade may install it.
|
|
325
|
+
|
|
326
|
+
WHY THIS EXISTS. `upgrade` only ever touched skills already RECORDED for the agent, so a skill
|
|
327
|
+
that is new in the catalog was published and never arrived: 0.17.0 shipped crow-be-human, every
|
|
328
|
+
CLI reported "atualizadas" and `doctor` stayed healthy while the skill existed nowhere on disk.
|
|
329
|
+
A release that the command the user runs cannot deliver is not delivered.
|
|
330
|
+
|
|
331
|
+
Returns {"missing": [names], "install": [names]}. The decision is per skill and read from the
|
|
332
|
+
GROUPS, because "has everything else" is vacuous — the set of missing names is by definition
|
|
333
|
+
everything it does not have. A missing skill is installed when some group of it is otherwise
|
|
334
|
+
COMPLETE here: the user took that group wholesale, and a new member belongs to it. A skill whose
|
|
335
|
+
every group is partial was curated by hand, so it is only reported, with the command.
|
|
336
|
+
"""
|
|
337
|
+
if source is None:
|
|
338
|
+
return {"missing": [], "install": []}
|
|
339
|
+
present = {r.get("name") for r in rows if r.get("name")}
|
|
340
|
+
present |= set(projected or [])
|
|
341
|
+
present |= {k.get("name") for k in (broken or []) if isinstance(k, dict) and k.get("name")}
|
|
342
|
+
missing = sorted(set(source.catalog) - present)
|
|
343
|
+
if not missing:
|
|
344
|
+
return {"missing": [], "install": []}
|
|
345
|
+
try:
|
|
346
|
+
from . import groups as groups_mod
|
|
347
|
+
available = [g for g in groups_mod.load(source) if g["name"] != groups_mod.ALL]
|
|
348
|
+
except (CrowError, OSError, ValueError, KeyError):
|
|
349
|
+
available = []
|
|
350
|
+
install = []
|
|
351
|
+
for name in missing:
|
|
352
|
+
for group in available:
|
|
353
|
+
members = [s for s in group["skills"] if s != name]
|
|
354
|
+
if name in group["skills"] and members and all(m in present for m in members):
|
|
355
|
+
install.append(name)
|
|
356
|
+
break
|
|
357
|
+
return {"missing": missing, "install": install}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def install_new(home, entry, names, agent, dry_run, trust):
|
|
361
|
+
"""Install catalog skills the agent is missing — only when it already has every other one.
|
|
362
|
+
|
|
363
|
+
The decision is read from the machine, not assumed: an agent holding the whole catalog minus the
|
|
364
|
+
new names has said "all of them", so a new skill belongs there. An agent with a SUBSET chose that
|
|
365
|
+
subset, and adding to it would be deciding for the user, so those names are only reported.
|
|
366
|
+
"""
|
|
367
|
+
if not names:
|
|
368
|
+
return
|
|
369
|
+
argv = ["install", *names, "--agents", agent, "--global", "--json", "--home", str(home),
|
|
370
|
+
"--dry-run" if dry_run else "--yes"]
|
|
371
|
+
if not trust:
|
|
372
|
+
argv.append("--no-trust")
|
|
373
|
+
from .cli import parser, run as cli_run
|
|
374
|
+
try:
|
|
375
|
+
data = cli_run(parser().parse_args(argv))
|
|
376
|
+
except CrowError as exc:
|
|
377
|
+
entry["newInCatalogError"] = {"code": exc.code, "message": str(exc)}
|
|
378
|
+
return
|
|
379
|
+
done = [c["name"] for c in data.get("changes") or [] if c.get("action") != "unchanged"]
|
|
380
|
+
entry["installedNew"] = sorted(done)
|
|
381
|
+
entry["updated"] = sorted({*entry.get("updated", []), *done})
|
|
382
|
+
if data.get("transaction"):
|
|
383
|
+
entry["newTransaction"] = data["transaction"]
|
|
384
|
+
|
|
322
385
|
def reinstall_behaviour(home, entry, dry_run, trust):
|
|
323
386
|
"""Codex without a working crow-behaviour (its projection is broken, no native copy): install it natively,
|
|
324
387
|
as setup does, so the guide and hooks keep a coordinator to point to."""
|
|
@@ -402,6 +465,9 @@ def skills_update(home, trust=True, dry_run=False):
|
|
|
402
465
|
from . import machine
|
|
403
466
|
entry = {"agent": agent, "target": target["path"], "updated": [], "unchanged": 0, "kept": list(broken), "error": None,
|
|
404
467
|
"via_uniflow": projected}
|
|
468
|
+
gap = catalog_gap(agent, home, current, rows, projected, broken)
|
|
469
|
+
if gap["missing"]:
|
|
470
|
+
entry["newInCatalog"] = gap["missing"]
|
|
405
471
|
if dry_run:
|
|
406
472
|
entry["dry_run"] = True
|
|
407
473
|
try:
|
|
@@ -413,6 +479,8 @@ def skills_update(home, trust=True, dry_run=False):
|
|
|
413
479
|
entry["error"] = {"code": exc.code, "message": str(exc)}
|
|
414
480
|
except (OSError, ValueError, KeyError, TypeError):
|
|
415
481
|
entry["error"] = {"code": "io_error", "message": "Falha de leitura/gravação; nada concluído para este agente."}
|
|
482
|
+
if gap["install"] and not entry.get("error"):
|
|
483
|
+
install_new(home, entry, gap["install"], agent, dry_run, trust)
|
|
416
484
|
if agent == "codex" and not entry.get("error"):
|
|
417
485
|
# Ramo mais provável de quem vinha da 0.14.0: no diretório do Codex só há links do
|
|
418
486
|
# Uniflow, e TODAS as cópias do codex estão na pasta compartilhada. Sem migrar aqui,
|
|
@@ -431,6 +499,9 @@ def skills_update(home, trust=True, dry_run=False):
|
|
|
431
499
|
names.append(r["name"])
|
|
432
500
|
entry = {"agent": agent, "target": target["path"], "updated": [], "unchanged": 0, "kept": kept + list(broken),
|
|
433
501
|
"error": None, "via_uniflow": projected}
|
|
502
|
+
gap = catalog_gap(agent, home, current, rows, projected, broken)
|
|
503
|
+
if gap["missing"]:
|
|
504
|
+
entry["newInCatalog"] = gap["missing"]
|
|
434
505
|
if dry_run:
|
|
435
506
|
entry["dry_run"] = True
|
|
436
507
|
if agent == "codex":
|
|
@@ -462,6 +533,8 @@ def skills_update(home, trust=True, dry_run=False):
|
|
|
462
533
|
entry["error"] = {"code": exc.code, "message": str(exc)}
|
|
463
534
|
except (OSError, ValueError, KeyError, TypeError):
|
|
464
535
|
entry["error"] = {"code": "io_error", "message": "Falha de leitura/gravação; nada concluído para este agente."}
|
|
536
|
+
if gap["install"] and not entry.get("error"):
|
|
537
|
+
install_new(home, entry, gap["install"], agent, dry_run, trust)
|
|
465
538
|
if agent == "codex" and not entry.get("error"):
|
|
466
539
|
# Mesma migração do setup, e pelo mesmo motivo: sem isto, uma máquina que só roda
|
|
467
540
|
# autoupdate/upgrade mantém a pasta compartilhada antiga para sempre. Depois da
|
package/crow_skills/broadcast.py
CHANGED
|
@@ -352,6 +352,14 @@ def edit_paths(host, tool, tool_input, cwd):
|
|
|
352
352
|
GIT_WORD = re.compile(r"^(?:\S*/)?git$")
|
|
353
353
|
DIR_WORD = re.compile(r"(?:^|[\s;&|()])(?:cd|[^\s;&|()]*/?git)(?:\s|$)")
|
|
354
354
|
TREE_SUBCOMMANDS = ("checkout", "switch", "rebase", "stash", "merge", "pull", "cherry-pick", "bisect", "restore", "clean")
|
|
355
|
+
# A git call is not by itself a sign of working in a clone: `git log`, `git status` and `git diff`
|
|
356
|
+
# are reading. Measured right after shipping occupancy(): a peer session that only INSPECTED two
|
|
357
|
+
# clones was still published as occupying them, because any git verb marked the clone as driven.
|
|
358
|
+
# Everything outside this list keeps marking occupancy, so an unknown verb errs toward warning.
|
|
359
|
+
READ_SUBCOMMANDS = ("log", "status", "diff", "show", "rev-parse", "rev-list", "ls-tree", "ls-files",
|
|
360
|
+
"ls-remote", "cat-file", "describe", "blame", "shortlog", "for-each-ref",
|
|
361
|
+
"name-rev", "merge-base", "symbolic-ref", "whatchanged", "grep", "count-objects",
|
|
362
|
+
"check-ignore", "verify-commit", "verify-tag", "var", "help", "version")
|
|
355
363
|
|
|
356
364
|
|
|
357
365
|
def tree_command(host, tool, tool_input, cwd=None):
|
|
@@ -423,7 +431,7 @@ def _note(visited, directory, source="cd"):
|
|
|
423
431
|
if visited is None or not directory or not directory.startswith("/"):
|
|
424
432
|
return
|
|
425
433
|
if directory in visited:
|
|
426
|
-
if source == "git":
|
|
434
|
+
if source == "git": # working outranks reading and the incidental cd
|
|
427
435
|
visited[directory] = "git"
|
|
428
436
|
return
|
|
429
437
|
if len(visited) < MAX_PATTERNS:
|
|
@@ -488,7 +496,8 @@ def _segment(words, here, targets, depth=0, visited=None):
|
|
|
488
496
|
if i >= len(words):
|
|
489
497
|
return here
|
|
490
498
|
sub, rest = words[i], words[i + 1:]
|
|
491
|
-
|
|
499
|
+
# "git" means the session WORKS in that clone; a read-only verb means it only looked.
|
|
500
|
+
_note(visited, directory, "read" if sub in READ_SUBCOMMANDS else "git")
|
|
492
501
|
if sub in TREE_SUBCOMMANDS or (sub == "reset" and "--hard" in rest):
|
|
493
502
|
if directory and directory not in targets:
|
|
494
503
|
targets.append(directory)
|
|
@@ -703,14 +712,21 @@ def fold(snap, event, touched=(), now=None, ppid=None, cli_pid=None, tty_runner=
|
|
|
703
712
|
repos = [r for r in repos if r["root"] != found["root"]]
|
|
704
713
|
entry = {"id": found["id"], "root": found["root"], "ts": now_iso(now),
|
|
705
714
|
"hits": int((previous or {}).get("hits") or 0) + 1}
|
|
706
|
-
|
|
715
|
+
source = dirs.get(directory) if isinstance(dirs, dict) else None
|
|
716
|
+
# TWO different questions about the same clone. `driven`: a git call named it, which is a
|
|
717
|
+
# stronger signal than an incidental `cd` and wins the eviction contest — a `git log` counts.
|
|
718
|
+
# `working`: the verb CHANGES something there, which is what occupancy() may warn about.
|
|
719
|
+
if (previous or {}).get("driven") or source in ("git", "read"):
|
|
707
720
|
entry["driven"] = True
|
|
721
|
+
if (previous or {}).get("working") or source == "git":
|
|
722
|
+
entry["working"] = True
|
|
708
723
|
repos.append(entry)
|
|
709
724
|
if repos or presence.get("repos") is not None:
|
|
710
725
|
# Eviction is not FIFO: a clone a git command actually drove, and one visited many times,
|
|
711
726
|
# outrank the incidental `cd` that would otherwise push it out of the eight slots.
|
|
712
|
-
keep = sorted(repos, key=lambda r: (bool(r.get("
|
|
713
|
-
parse_time(r.get("ts")) or 0),
|
|
727
|
+
keep = sorted(repos, key=lambda r: (bool(r.get("working")), bool(r.get("driven")),
|
|
728
|
+
int(r.get("hits") or 0), parse_time(r.get("ts")) or 0),
|
|
729
|
+
reverse=True)[:MAX_REPOS]
|
|
714
730
|
presence["repos"] = [r for r in repos if r in keep]
|
|
715
731
|
snap["presence"] = presence
|
|
716
732
|
return snap
|
|
@@ -929,8 +945,8 @@ def occupancy(snap, now=None, window=None):
|
|
|
929
945
|
ids.add(repo["id"])
|
|
930
946
|
if repo.get("root"):
|
|
931
947
|
roots.add(repo["root"])
|
|
932
|
-
for entry in presence.get("repos") or []: # only the clones a git command drove
|
|
933
|
-
if not isinstance(entry, dict) or not entry.get("
|
|
948
|
+
for entry in presence.get("repos") or []: # only the clones a WRITING git command drove
|
|
949
|
+
if not isinstance(entry, dict) or not entry.get("working"):
|
|
934
950
|
continue
|
|
935
951
|
if now - (parse_time(entry.get("ts")) or 0) > window:
|
|
936
952
|
continue
|
package/crow_skills/machine.py
CHANGED
|
@@ -778,6 +778,17 @@ def upgrade_summary(skills, dry_run):
|
|
|
778
778
|
k["name"] + " --agents " + agent + " --global --yes")
|
|
779
779
|
else:
|
|
780
780
|
item["needs_user"].append(k["name"] + ": " + k["status"])
|
|
781
|
+
# Skill nova no catálogo: instalada quando o agente já tinha todas as outras, e NOMEADA com o
|
|
782
|
+
# comando quando ele tem um subconjunto escolhido. Antes disto o upgrade dizia "atualizadas" e
|
|
783
|
+
# a skill não existia em disco em lugar nenhum.
|
|
784
|
+
item["installed_new"] = s.get("installedNew") or []
|
|
785
|
+
pending = [n for n in (s.get("newInCatalog") or []) if n not in item["installed_new"]]
|
|
786
|
+
if pending:
|
|
787
|
+
item["needs_user"].append(str(len(pending)) + " skill(s) nova(s) no catálogo não instalada(s) aqui (" +
|
|
788
|
+
", ".join(pending[:3]) + "); este agente tem um subconjunto escolhido. Para instalar: "
|
|
789
|
+
"crow-skills install " + " ".join(pending[:3]) + " --agents " + agent + " --global --yes")
|
|
790
|
+
if s.get("newInCatalogError"):
|
|
791
|
+
item["needs_user"].insert(0, "skill nova do catálogo não instalada: " + s["newInCatalogError"]["message"])
|
|
781
792
|
if s.get("error"):
|
|
782
793
|
item["needs_user"].insert(0, s["error"]["message"])
|
|
783
794
|
item["state"] = "fail" if s.get("error") else ("warn" if item["needs_user"] else ("plan" if dry_run and (item["changed"] or item["removed"]) else "ok"))
|
|
@@ -904,9 +915,14 @@ def render_upgrade(data, stream=None):
|
|
|
904
915
|
changed = ", ".join(item["changed"]) if len(item["changed"]) <= 3 else str(len(item["changed"])) + " skills"
|
|
905
916
|
removed = ((" · " if changed else "") + str(len(item["removed"])) + (" a remover" if dry else " removidas") +
|
|
906
917
|
" (duplicavam o Uniflow)") if item.get("removed") else ""
|
|
918
|
+
fresh = item.get("installed_new") or []
|
|
919
|
+
# Dito na própria linha do CLI: uma skill nova do catálogo não é "atualizada", é instalada
|
|
920
|
+
# agora — e foi exatamente isso que faltou quando a 0.17.0 publicou a crow-be-human.
|
|
921
|
+
new_text = ((" · " if changed or removed else "") + (("instalaria " if dry else "instaladas ") +
|
|
922
|
+
("novas: " + ", ".join(fresh[:3]) if len(fresh) <= 3 else str(len(fresh)) + " novas"))) if fresh else ""
|
|
907
923
|
rows.append([ui.mark(STATE_MARK[item["state"]], stream), item["label"],
|
|
908
|
-
((("a atualizar: " if dry else "atualizadas: ") + changed) if changed else ("" if removed else "sem mudanças"))
|
|
909
|
-
+ removed,
|
|
924
|
+
((("a atualizar: " if dry else "atualizadas: ") + changed) if changed else ("" if removed or new_text else "sem mudanças"))
|
|
925
|
+
+ removed + new_text,
|
|
910
926
|
str(item["unchanged"]), str(item.get("via_uniflow", 0)), str(len(item["kept"]))])
|
|
911
927
|
if rows:
|
|
912
928
|
lines.append("")
|
package/package.json
CHANGED
package/release.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"crow_skills/__init__.py": {
|
|
19
19
|
"executable": false,
|
|
20
20
|
"kind": "file",
|
|
21
|
-
"sha256": "
|
|
21
|
+
"sha256": "430c470bc3c5bc90aecb240c2756f9d1d7465a1e8828c36e01451eb15b211dbd"
|
|
22
22
|
},
|
|
23
23
|
"crow_skills/__main__.py": {
|
|
24
24
|
"executable": false,
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
"crow_skills/autoupdate.py": {
|
|
34
34
|
"executable": false,
|
|
35
35
|
"kind": "file",
|
|
36
|
-
"sha256": "
|
|
36
|
+
"sha256": "f5a994f8eb47ed77861cf2e512d7660654ba3e8cf9381229add678180ae14e0d"
|
|
37
37
|
},
|
|
38
38
|
"crow_skills/broadcast.py": {
|
|
39
39
|
"executable": false,
|
|
40
40
|
"kind": "file",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "135fb80925dea8e6c0790ee830c6b7e440cc5c0f116a4c9dcc4cf36bab1c22e3"
|
|
42
42
|
},
|
|
43
43
|
"crow_skills/cli.py": {
|
|
44
44
|
"executable": false,
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"crow_skills/machine.py": {
|
|
74
74
|
"executable": false,
|
|
75
75
|
"kind": "file",
|
|
76
|
-
"sha256": "
|
|
76
|
+
"sha256": "83d3f6fa0e296b4de87d90ff90ff50784241cf346bc1947ab78dd415409ebc0d"
|
|
77
77
|
},
|
|
78
78
|
"crow_skills/npm_entry.py": {
|
|
79
79
|
"executable": false,
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"package.json": {
|
|
129
129
|
"executable": false,
|
|
130
130
|
"kind": "file",
|
|
131
|
-
"sha256": "
|
|
131
|
+
"sha256": "84ebc44ccadb19d2e6b1640994476d016c6ebef62bb0e2d79fdd14e6643f5141"
|
|
132
132
|
},
|
|
133
133
|
"skills/crow-accessibility/SKILL.md": {
|
|
134
134
|
"executable": false,
|
|
@@ -721,7 +721,7 @@
|
|
|
721
721
|
"sha256": "e37132ef8b060548191fbf14a5a18a9dd3acd84ea9a35e802174455b9e374d3d"
|
|
722
722
|
}
|
|
723
723
|
},
|
|
724
|
-
"revision": "
|
|
724
|
+
"revision": "4c059d58b0bff2c5c3cfcf019667bb1ee93cd08d",
|
|
725
725
|
"schema_version": 1,
|
|
726
|
-
"version": "0.
|
|
726
|
+
"version": "0.20.0"
|
|
727
727
|
}
|