@andespindola/crow-skills 0.7.1 → 0.9.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 +6 -5
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/autoupdate.py +398 -0
- package/crow_skills/cli.py +99 -3
- package/crow_skills/events.py +133 -0
- package/crow_skills/guide.py +7 -1
- package/crow_skills/hook.py +616 -0
- package/crow_skills/integrations.py +460 -63
- package/crow_skills/progress.py +157 -15
- package/crow_skills/resources.py +713 -0
- package/crow_skills/source.py +40 -0
- package/crow_skills/storage.py +5 -4
- package/docs/cli.md +144 -17
- package/package.json +1 -1
- package/release.json +56 -31
- package/skills/crow-accessibility/SKILL.md +2 -0
- package/skills/crow-api-test/SKILL.md +2 -0
- package/skills/crow-backend/SKILL.md +2 -0
- package/skills/crow-behaviour/SKILL.md +4 -3
- package/skills/crow-behaviour/references/orchestration.md +4 -0
- package/skills/crow-behaviour/references/progress-ledger.md +20 -3
- package/skills/crow-behaviour/references/resources.md +44 -0
- package/skills/crow-behaviour/scripts/crow_ledger.py +120 -4
- package/skills/crow-browser/SKILL.md +2 -0
- package/skills/crow-debug/SKILL.md +2 -0
- package/skills/crow-frontend/SKILL.md +2 -0
- package/skills/crow-infra/SKILL.md +2 -0
- package/skills/crow-interface/SKILL.md +2 -0
- package/skills/crow-media/SKILL.md +2 -0
- package/skills/crow-native-mobile/SKILL.md +2 -0
- package/skills/crow-performance/SKILL.md +2 -0
- package/skills/crow-qa/SKILL.md +2 -0
- package/skills/crow-release/SKILL.md +2 -0
- package/skills/crow-reverse/SKILL.md +2 -0
- package/skills/crow-security-scan/SKILL.md +2 -0
package/crow_skills/progress.py
CHANGED
|
@@ -31,6 +31,20 @@ MAX_LEDGER = 2 * 1024 * 1024
|
|
|
31
31
|
MAX_LOG_LINE = 8 * 1024 * 1024
|
|
32
32
|
RECENT_DONE = 300 # seconds a finished run stays visible in compact views
|
|
33
33
|
STALE = 2 * 3600 # a "running" ledger untouched for this long is treated as abandoned
|
|
34
|
+
MAX_AGENT_ROWS = 6 # process rows shown in a status line before "+N"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def hook_module():
|
|
38
|
+
"""The mode/event helpers: the package module, or crow_hook.py copied beside this script."""
|
|
39
|
+
try:
|
|
40
|
+
from . import hook # type: ignore[attr-defined]
|
|
41
|
+
return hook
|
|
42
|
+
except ImportError:
|
|
43
|
+
try:
|
|
44
|
+
import crow_hook as hook # sibling copy in the integrations directory
|
|
45
|
+
return hook
|
|
46
|
+
except ImportError:
|
|
47
|
+
return None
|
|
34
48
|
|
|
35
49
|
|
|
36
50
|
# --- ledger -------------------------------------------------------------------------
|
|
@@ -69,7 +83,7 @@ def validate(data):
|
|
|
69
83
|
"status": data.get("status") if data.get("status") in ("running",) + FINISHED else "running",
|
|
70
84
|
"startedAt": data.get("startedAt"), "updatedAt": data.get("updatedAt"), "endedAt": data.get("endedAt"),
|
|
71
85
|
"session": data.get("session") if isinstance(data.get("session"), dict) else {},
|
|
72
|
-
"phases": [], "units": []}
|
|
86
|
+
"phases": [], "units": [], "resources": resources_decision(data.get("resources"))}
|
|
73
87
|
for phase in phases:
|
|
74
88
|
if isinstance(phase, dict) and isinstance(phase.get("id"), str):
|
|
75
89
|
run["phases"].append({"id": phase["id"], "title": safe(phase.get("title") or phase["id"]),
|
|
@@ -93,6 +107,42 @@ def validate(data):
|
|
|
93
107
|
return run
|
|
94
108
|
|
|
95
109
|
|
|
110
|
+
RESOURCE_MODES = ("parallel", "reduced", "sequential")
|
|
111
|
+
RESOURCE_LEVELS = ("normal", "warn", "critical", "unknown")
|
|
112
|
+
RESOURCE_SOURCES = ("probe", "os", "assumed")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resources_decision(value):
|
|
116
|
+
"""The optional run-level ``resources`` field (0.9.0): the parallelism decision recorded
|
|
117
|
+
before a fan-out. Ledgers without it, or with an invalid one, read as None."""
|
|
118
|
+
if not isinstance(value, dict) or value.get("mode") not in RESOURCE_MODES:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def count(key):
|
|
122
|
+
number = value.get(key)
|
|
123
|
+
return number if isinstance(number, int) and not isinstance(number, bool) and 0 <= number <= 256 else None
|
|
124
|
+
|
|
125
|
+
return {"mode": value["mode"], "pressure": value.get("pressure") if value.get("pressure") in RESOURCE_LEVELS else None,
|
|
126
|
+
"source": value.get("source") if value.get("source") in RESOURCE_SOURCES else None,
|
|
127
|
+
"maxParallelAgents": count("maxParallelAgents"), "maxParallelHeavyJobs": count("maxParallelHeavyJobs"),
|
|
128
|
+
"maxWorkersPerJob": count("maxWorkersPerJob"),
|
|
129
|
+
"checkedAt": value.get("checkedAt") if isinstance(value.get("checkedAt"), str) else None,
|
|
130
|
+
"note": safe(value.get("note") or "", 160)}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def resources_line(decision, unicode=True):
|
|
134
|
+
if not decision:
|
|
135
|
+
return ""
|
|
136
|
+
parts = [decision["mode"]]
|
|
137
|
+
if decision["maxParallelAgents"] is not None:
|
|
138
|
+
parts.append(("≤" if unicode else "<=") + str(decision["maxParallelAgents"]) + " agente(s)")
|
|
139
|
+
if decision["maxParallelHeavyJobs"] is not None:
|
|
140
|
+
parts.append(("≤" if unicode else "<=") + str(decision["maxParallelHeavyJobs"]) + " job(s) pesado(s)")
|
|
141
|
+
if decision["pressure"]:
|
|
142
|
+
parts.append("pressão " + decision["pressure"])
|
|
143
|
+
return "recursos: " + (" · " if unicode else " - ").join(parts) + (" (" + decision["note"] + ")" if decision["note"] else "")
|
|
144
|
+
|
|
145
|
+
|
|
96
146
|
def load(path):
|
|
97
147
|
path = Path(path)
|
|
98
148
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_LEDGER:
|
|
@@ -501,6 +551,9 @@ def render(run, usage=None, now=None, color=False, unicode=True, width=20):
|
|
|
501
551
|
bar(info["progress"], width, unicode) + " " + str(int(info["progress"] * 100)).rjust(3) + "% " +
|
|
502
552
|
str(info["done"]) + "/" + str(info["total"]) + " unidades" +
|
|
503
553
|
(" - " + str(info["failed"]) + " falha(s)" if info["failed"] else "") + " " + token_text(usage, unicode)]
|
|
554
|
+
if run.get("resources"):
|
|
555
|
+
lines.append(paint("dim", resources_line(run["resources"], unicode)))
|
|
556
|
+
lines += ["", *agent_rows(run, unicode, counts=False), ""]
|
|
504
557
|
phase_ids = [p["id"] for p in run["phases"]]
|
|
505
558
|
groups = [(p, [u for u in run["units"] if u["phase"] == p["id"]]) for p in run["phases"]]
|
|
506
559
|
loose = [u for u in run["units"] if u["phase"] not in phase_ids]
|
|
@@ -527,6 +580,50 @@ def render(run, usage=None, now=None, color=False, unicode=True, width=20):
|
|
|
527
580
|
return "\n".join(lines)
|
|
528
581
|
|
|
529
582
|
|
|
583
|
+
def owner_label(owner):
|
|
584
|
+
"""subagent:leitor -> leitor; workflow:x and sequential stay as written."""
|
|
585
|
+
owner = owner or "main"
|
|
586
|
+
return owner.split(":", 1)[1] if owner.startswith("subagent:") and owner[9:] else owner
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def agent_rows(run, unicode=True, limit=None, counts=True):
|
|
590
|
+
"""Live process list, like the host's own agent list:
|
|
591
|
+
|
|
592
|
+
● main <what the coordinator is doing>
|
|
593
|
+
○ <owner> <running unit>
|
|
594
|
+
✓ 3 concluídas · ✗ 1 falhou · 2 pendentes
|
|
595
|
+
|
|
596
|
+
Rows come only from the ledger (units the coordinator recorded); nothing is invented.
|
|
597
|
+
"""
|
|
598
|
+
dot, ring = ("●", "○") if unicode else ("*", "o")
|
|
599
|
+
sep = " · " if unicode else " - "
|
|
600
|
+
running = [u for u in run["units"] if u["status"] == "running"]
|
|
601
|
+
mine = [u for u in running if owner_label(u["owner"]) in ("main", "sequential")]
|
|
602
|
+
others = [u for u in running if u not in mine]
|
|
603
|
+
head = dot + " main"
|
|
604
|
+
if mine:
|
|
605
|
+
head += " " + safe(mine[0]["title"], 60) + (" (+" + str(len(mine) - 1) + ")" if len(mine) > 1 else "")
|
|
606
|
+
elif run["status"] == "running":
|
|
607
|
+
head += " " + safe(run["title"], 60)
|
|
608
|
+
lines = [head]
|
|
609
|
+
shown = others if limit is None else others[:limit]
|
|
610
|
+
for unit in shown:
|
|
611
|
+
lines.append(ring + " " + safe(owner_label(unit["owner"]), 32) + " " + safe(unit["title"], 60))
|
|
612
|
+
if limit is not None and len(others) > limit:
|
|
613
|
+
lines.append(ring + " +" + str(len(others) - limit))
|
|
614
|
+
if counts:
|
|
615
|
+
done = sum(u["status"] == "done" for u in run["units"])
|
|
616
|
+
failed = sum(u["status"] == "failed" for u in run["units"])
|
|
617
|
+
pending = sum(u["status"] == "pending" for u in run["units"])
|
|
618
|
+
parts = [("✓ " if unicode else "ok ") + str(done) + " concluída(s)"]
|
|
619
|
+
if failed:
|
|
620
|
+
parts.append(("✗ " if unicode else "x ") + str(failed) + " falha(s)")
|
|
621
|
+
if pending:
|
|
622
|
+
parts.append(str(pending) + " pendente(s)")
|
|
623
|
+
lines.append(sep.join(parts))
|
|
624
|
+
return lines
|
|
625
|
+
|
|
626
|
+
|
|
530
627
|
def compact(run, usage=None, unicode=True, width=10):
|
|
531
628
|
info = summary(run)
|
|
532
629
|
mark = "✓" if run["status"] == "done" else ("✗" if run["status"] in ("failed", "cancelled") else "▸")
|
|
@@ -580,19 +677,53 @@ def watch(cwd, home, run_id=None, interval=1.0, stream=None, linger=5.0, max_fra
|
|
|
580
677
|
return 0
|
|
581
678
|
|
|
582
679
|
|
|
583
|
-
def
|
|
584
|
-
|
|
680
|
+
def mode_text(home, unicode=True):
|
|
681
|
+
hook = hook_module()
|
|
682
|
+
if hook is None:
|
|
683
|
+
return ""
|
|
684
|
+
try:
|
|
685
|
+
return hook.indicator(hook.read_mode(home)["mode"], unicode)
|
|
686
|
+
except (OSError, ValueError, KeyError):
|
|
687
|
+
return ""
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def memory_text(home, unicode=True):
|
|
691
|
+
"""'mem ▲ 85% ×2' when the machine is under pressure or parallelism is reduced; else ''."""
|
|
692
|
+
hook = hook_module()
|
|
693
|
+
module = hook.resources_module() if hook is not None and hasattr(hook, "resources_module") else None
|
|
694
|
+
if module is None:
|
|
695
|
+
return ""
|
|
696
|
+
try:
|
|
697
|
+
return module.segment(module.probe(home, processes=False, use_cache=True), unicode)
|
|
698
|
+
except Exception: # noqa: BLE001 - the probe never breaks a status line
|
|
699
|
+
return ""
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def statusline(home, stdin_text, chain=None, host="claude"):
|
|
703
|
+
"""Status line for Claude Code and Antigravity (agy).
|
|
704
|
+
|
|
705
|
+
First line: the previous (chained) status line, the mode indicator and the compact
|
|
706
|
+
run; then one row per running unit of the ledger, like the host's agent list. agy
|
|
707
|
+
stacks it under its default line (stack_with_default), so no chain is needed there.
|
|
708
|
+
"""
|
|
585
709
|
try:
|
|
586
710
|
payload = json.loads(stdin_text) if stdin_text.strip() else {}
|
|
587
711
|
except ValueError:
|
|
588
712
|
payload = {}
|
|
589
713
|
if not isinstance(payload, dict):
|
|
590
714
|
payload = {}
|
|
591
|
-
|
|
715
|
+
workspace = payload.get("workspace") if isinstance(payload.get("workspace"), dict) else {}
|
|
716
|
+
cwd = workspace.get("current_dir") or payload.get("cwd") or os.getcwd()
|
|
592
717
|
transcript = payload.get("transcript_path")
|
|
593
|
-
session = payload.get("session_id")
|
|
718
|
+
session = payload.get("session_id") or payload.get("conversation_id")
|
|
594
719
|
if not (isinstance(session, str) and session):
|
|
595
720
|
session = Path(transcript).stem if isinstance(transcript, str) and transcript.endswith(".jsonl") else None
|
|
721
|
+
hook = hook_module()
|
|
722
|
+
if hook is not None:
|
|
723
|
+
try:
|
|
724
|
+
hook.update_metrics(home, host, payload)
|
|
725
|
+
except Exception: # noqa: BLE001 - metrics never break the status line
|
|
726
|
+
pass
|
|
596
727
|
previous = ""
|
|
597
728
|
if chain:
|
|
598
729
|
try:
|
|
@@ -603,9 +734,10 @@ def statusline(home, stdin_text, chain=None):
|
|
|
603
734
|
except (OSError, subprocess.SubprocessError):
|
|
604
735
|
previous = ""
|
|
605
736
|
run = select(discover(cwd, home), cwd, strict=True, session=session)
|
|
606
|
-
|
|
737
|
+
parts = [p for p in (mode_text(home), memory_text(home)) if p]
|
|
738
|
+
rows = []
|
|
607
739
|
if active(run):
|
|
608
|
-
if run["host"] == "claude" and not run["session"].get("id") and session:
|
|
740
|
+
if run["host"] == "claude" and host == "claude" and not run["session"].get("id") and session:
|
|
609
741
|
# The status line knows its own session: exact usage instead of every session in the directory.
|
|
610
742
|
try:
|
|
611
743
|
usage = claude_tokens(dict(run, session={"id": session}), Path(home), *window(run))
|
|
@@ -613,19 +745,28 @@ def statusline(home, stdin_text, chain=None):
|
|
|
613
745
|
usage = None
|
|
614
746
|
else:
|
|
615
747
|
usage = tokens(run, home)
|
|
616
|
-
|
|
748
|
+
parts.append(compact(run, usage))
|
|
749
|
+
if run["status"] == "running":
|
|
750
|
+
rows = agent_rows(run, limit=MAX_AGENT_ROWS, counts=False)
|
|
751
|
+
if len(rows) == 1:
|
|
752
|
+
rows = [] # only "● main": the compact segment already says it
|
|
753
|
+
ours = " │ ".join(parts)
|
|
617
754
|
if not previous.strip():
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
755
|
+
first_line = ours
|
|
756
|
+
elif not ours:
|
|
757
|
+
first_line = previous
|
|
758
|
+
else:
|
|
759
|
+
first_line = previous + " │ " + ours
|
|
760
|
+
return "\n".join([first_line, *rows]) if rows else first_line
|
|
622
761
|
|
|
623
762
|
|
|
624
763
|
def tmux_segment(home, cwd):
|
|
764
|
+
head = " · ".join(p for p in (mode_text(home), memory_text(home)) if p)
|
|
625
765
|
run = select(discover(cwd, home), cwd, strict=True)
|
|
626
766
|
if not active(run):
|
|
627
|
-
return ""
|
|
628
|
-
|
|
767
|
+
return head.replace("#", "##")
|
|
768
|
+
text = compact(run, tokens(run, home), unicode=True, width=8)
|
|
769
|
+
return ((head + " · " if head else "") + text).replace("#", "##")
|
|
629
770
|
|
|
630
771
|
|
|
631
772
|
def main(argv=None):
|
|
@@ -636,6 +777,7 @@ def main(argv=None):
|
|
|
636
777
|
p.add_argument("--cwd", default=None)
|
|
637
778
|
p.add_argument("--home", default=None)
|
|
638
779
|
p.add_argument("--chain-file", default=None, help="JSON com o comando de status line anterior, encadeado antes do crow")
|
|
780
|
+
p.add_argument("--host", default="claude", choices=("claude", "agy"), help="CLI que chama a status line")
|
|
639
781
|
args = p.parse_args(argv)
|
|
640
782
|
home = Path(args.home).expanduser() if args.home else Path.home()
|
|
641
783
|
cwd = args.cwd or os.getcwd()
|
|
@@ -646,7 +788,7 @@ def main(argv=None):
|
|
|
646
788
|
chain = json.loads(Path(args.chain_file).read_text(encoding="utf-8")).get("command")
|
|
647
789
|
except (OSError, ValueError, AttributeError):
|
|
648
790
|
chain = None
|
|
649
|
-
print(statusline(home, sys.stdin.read() if not sys.stdin.isatty() else "", chain))
|
|
791
|
+
print(statusline(home, sys.stdin.read() if not sys.stdin.isatty() else "", chain, args.host))
|
|
650
792
|
return 0
|
|
651
793
|
if args.mode == "tmux":
|
|
652
794
|
print(tmux_segment(home, cwd))
|