@andespindola/crow-skills 0.8.0 → 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 +2 -2
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/cli.py +10 -0
- package/crow_skills/events.py +25 -3
- package/crow_skills/hook.py +57 -1
- package/crow_skills/integrations.py +35 -2
- package/crow_skills/progress.py +55 -5
- package/crow_skills/resources.py +713 -0
- package/docs/cli.md +51 -3
- package/package.json +1 -1
- package/release.json +40 -30
- 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 +1 -0
- package/skills/crow-behaviour/references/orchestration.md +4 -0
- package/skills/crow-behaviour/references/progress-ledger.md +3 -0
- package/skills/crow-behaviour/references/resources.md +44 -0
- package/skills/crow-behaviour/scripts/crow_ledger.py +75 -2
- 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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Skills com escopo explícito, conhecimento especializado preservado e fluxos comuns reutilizáveis.
|
|
4
4
|
|
|
5
|
-
**Versão 0.
|
|
5
|
+
**Versão 0.9.0** ([mudanças](CHANGELOG.md)): as skills controlam a memória da máquina: `crow-skills resources` mede memória, pressão e os CLIs de agente em execução e recomenda quantos agentes, jobs pesados e workers rodar em paralelo; a crow-behaviour consulta essa recomendação antes de cada fan-out (sob `warn` reduz, sob `critical` segue sequencial), limita workers de builds e testes, reutiliza um dev server por projeto e encerra o que abriu, e 15 especialistas que sobem processos pesados ganharam a regra correspondente; o hook de prompt e as status lines só avisam quando a máquina está sob pressão. A 0.8.0 trouxe `crow-skills mode on|off`, que liga e desliga a crow-behaviour em todas as sessões e CLIs (Claude Code, Codex, Gemini CLI, Antigravity, Devin) a partir do próximo prompt, com `/crow on|off` dentro da sessão e o indicador `▸▸ crow on` na status line; a crow-behaviour mostra o progresso como a lista de agentes do host (`● main`, `○ dono atividade`); `crow-skills events` publica um barramento de eventos dos hooks para terminais; `crow-skills autoupdate on` atualiza o CLI e as skills gerenciadas todo dia e no login. A 0.7.1 trouxe as correções da revalidação no Codex às skills especialistas; a 0.7.0 tornou o Uniflow agente de instalação (`--agents uniflow`); a 0.6.1 passou a mostrar o progresso da crow-behaviour na própria conversa; a 0.6.0 trouxe as regras de entrega das 32 skills especialistas, a partir da [validação no Codex](docs/evaluations/2026-10-26-codex.md).
|
|
6
6
|
|
|
7
7
|
**Reconstrução em andamento.** A primeira versão com 32 contratos genéricos não atende à preservação de capacidades solicitada. Não use esse número nem o mapeamento por categoria como prova de substituição das originais. A revisão passa a abranger cada definição e seus recursos, modos e dependências, sem número fixo de skills ou limite artificial de linhas. Veja [critérios e progresso](docs/reconstruction.md).
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Exemplos:
|
|
|
30
30
|
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. No [Uniflow](docs/cli.md#uniflow), `--agents uniflow --global` grava as skills no store dele (`~/.config/uniflow/skills`), sem tocar no resto da configuração. 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. As integrações de interface (status lines do Claude Code, do Codex e do Antigravity, hooks do modo crow em cada CLI, comando `/crow`, segmento tmux) seguem o mesmo fluxo, preservando a configuração existente; `--no-integrations` desliga. O [barramento de eventos](docs/events.md) e a atualização automática (`crow-skills autoupdate on`) estão no [CLI](docs/cli.md).
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
npm install -g @andespindola/crow-skills@0.
|
|
33
|
+
npm install -g @andespindola/crow-skills@0.9.0
|
|
34
34
|
crow-skills mode
|
|
35
35
|
crow-skills progress
|
|
36
36
|
./bin/crow-skills list
|
package/crow_skills/__init__.py
CHANGED
package/crow_skills/cli.py
CHANGED
|
@@ -12,6 +12,7 @@ from . import guide
|
|
|
12
12
|
from . import hook
|
|
13
13
|
from . import integrations
|
|
14
14
|
from . import progress
|
|
15
|
+
from . import resources
|
|
15
16
|
from .agents import AGENTS, destinations, inventory
|
|
16
17
|
from .source import CrowError, Source, valid_name
|
|
17
18
|
from .storage import Store, checked
|
|
@@ -77,6 +78,9 @@ def parser():
|
|
|
77
78
|
child.add_argument("action", nargs="?", default="snapshot", choices=("snapshot", "tail"))
|
|
78
79
|
child.add_argument("--limit", type=int, default=50, help="tail: quantos eventos (padrão 50; 0 = todos os retidos)")
|
|
79
80
|
child.add_argument("--follow", action="store_true", help="tail: transmitir eventos novos em JSONL até Ctrl-C")
|
|
81
|
+
child = sub.add_parser("resources", parents=[common],
|
|
82
|
+
help="Memória, pressão, agentes em execução e paralelismo recomendado (somente leitura)")
|
|
83
|
+
child.add_argument("--no-processes", action="store_true", help="Não varrer os processos dos CLIs de agente (mais rápido)")
|
|
80
84
|
child = sub.add_parser("autoupdate", parents=[common], help="Atualização diária e no login do crow-skills e das skills gerenciadas")
|
|
81
85
|
child.add_argument("action", choices=("on", "off", "status", "run"))
|
|
82
86
|
child.add_argument("--git-pull", action="store_true", help="on: também atualizar um checkout git limpo em main/master (git pull --ff-only)")
|
|
@@ -460,6 +464,10 @@ def run(args):
|
|
|
460
464
|
return mode_command(args, home)
|
|
461
465
|
if args.command == "events":
|
|
462
466
|
return events_command(args, home, store)
|
|
467
|
+
if args.command == "resources":
|
|
468
|
+
data = resources.probe(home, processes=not args.no_processes)
|
|
469
|
+
data["text"] = resources.render(data)
|
|
470
|
+
return data
|
|
463
471
|
if args.command == "autoupdate":
|
|
464
472
|
return autoupdate_command(args, home, store, interactive)
|
|
465
473
|
if args.command in ("status", "doctor"):
|
|
@@ -586,6 +594,8 @@ def emit(command, data, machine):
|
|
|
586
594
|
elif command == "progress":
|
|
587
595
|
if data is not None:
|
|
588
596
|
print(data["text"] or "crow: nenhuma execução registrada neste diretório.")
|
|
597
|
+
elif command == "resources":
|
|
598
|
+
print(data["text"])
|
|
589
599
|
elif command == "mode":
|
|
590
600
|
print(data["indicator"] + " — " + data["applies"])
|
|
591
601
|
elif command == "events":
|
package/crow_skills/events.py
CHANGED
|
@@ -11,7 +11,7 @@ import json
|
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
13
|
|
|
14
|
-
from . import autoupdate, hook, progress
|
|
14
|
+
from . import autoupdate, hook, progress, resources
|
|
15
15
|
from .source import CrowError
|
|
16
16
|
|
|
17
17
|
SNAPSHOT_SCHEMA = "crow-snapshot/1"
|
|
@@ -29,7 +29,7 @@ def ledger_runs(home, cwds):
|
|
|
29
29
|
runs.append({"id": run["id"], "title": run["title"], "host": run["host"], "cwd": run["cwd"], "status": run["status"],
|
|
30
30
|
"session": run["session"].get("id"), "path": run["path"], "updatedAt": run.get("updatedAt"),
|
|
31
31
|
"done": info["done"], "failed": info["failed"], "total": info["total"], "running": info["running"],
|
|
32
|
-
"progress": info["progress"],
|
|
32
|
+
"progress": info["progress"], "resources": run.get("resources"),
|
|
33
33
|
"agents": [{"owner": progress.owner_label(u["owner"]), "unit": u["id"], "activity": u["title"]}
|
|
34
34
|
for u in run["units"] if u["status"] == "running"]})
|
|
35
35
|
return runs
|
|
@@ -54,9 +54,25 @@ def snapshot(home, store):
|
|
|
54
54
|
except CrowError as exc:
|
|
55
55
|
update = {"enabled": None, "error": exc.code}
|
|
56
56
|
mode = hook.read_mode(home)
|
|
57
|
+
try:
|
|
58
|
+
machine = resources_field(home)
|
|
59
|
+
except Exception: # noqa: BLE001 - the snapshot never fails because of the probe
|
|
60
|
+
machine = None
|
|
57
61
|
return {"schema": SNAPSHOT_SCHEMA, "generatedAt": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
58
62
|
"mode": {k: mode[k] for k in ("mode", "updatedAt", "updatedBy", "source")}, "indicator": hook.indicator(mode["mode"]),
|
|
59
|
-
"sessions": live, "runs": runs, "autoupdate": update}
|
|
63
|
+
"sessions": live, "runs": runs, "autoupdate": update, "resources": machine}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def resources_field(home):
|
|
67
|
+
"""crow-resources/1 subset for terminals: memory, pressure and the recommendation.
|
|
68
|
+
|
|
69
|
+
Added in 0.9.0 without changing crow-snapshot/1: consumers that do not know the
|
|
70
|
+
field ignore it; the field carries its own schema for future changes.
|
|
71
|
+
"""
|
|
72
|
+
data = resources.probe(home, processes=False, use_cache=True)
|
|
73
|
+
return {"schema": data["schema"], "generatedAt": data["generatedAt"], "cached": data["cached"],
|
|
74
|
+
"memory": data["memory"], "pressure": {"level": data["pressure"]["level"], "reasons": data["pressure"]["reasons"]},
|
|
75
|
+
"recommendation": data["recommendation"], "alert": resources.alert(data), "segment": resources.segment(data)}
|
|
60
76
|
|
|
61
77
|
|
|
62
78
|
def render(data, unicode=True):
|
|
@@ -75,6 +91,12 @@ def render(data, unicode=True):
|
|
|
75
91
|
unicode, counts=False)
|
|
76
92
|
lines.append(("✓ " if unicode else "ok ") + str(run["done"]) + "/" + str(run["total"]) + " " + run["title"])
|
|
77
93
|
update = data["autoupdate"]
|
|
94
|
+
machine = data.get("resources")
|
|
95
|
+
if machine and machine.get("alert"):
|
|
96
|
+
rec = machine["recommendation"]
|
|
97
|
+
lines.append("")
|
|
98
|
+
lines.append("memória: " + (machine["segment"] or "") + " · pressão " + machine["pressure"]["level"] + " · " + rec["mode"] +
|
|
99
|
+
" (até " + str(rec["maxParallelAgents"]) + " agente(s), " + str(rec["maxParallelHeavyJobs"]) + " job(s) pesado(s))")
|
|
78
100
|
lines.append("")
|
|
79
101
|
lines.append("autoupdate: " + ("ligado" if update.get("enabled") else "desligado") +
|
|
80
102
|
((" · última execução " + str(update["lastRun"]["startedAt"]) + (" ok" if update["lastRun"]["ok"] else " com falha"))
|
package/crow_skills/hook.py
CHANGED
|
@@ -13,6 +13,11 @@ permission mode, event type, tool name, counters and timestamps.
|
|
|
13
13
|
|
|
14
14
|
crow_hook.py hook --host claude|codex|gemini|agy|devin --event NAME [--home DIR]
|
|
15
15
|
crow_hook.py mode on|off|status [--home DIR] [--by HOST] [--json]
|
|
16
|
+
|
|
17
|
+
On a prompt event the hook also appends one memory line to the context, only
|
|
18
|
+
when the machine is under pressure or the recommended parallelism is not
|
|
19
|
+
"parallel" (crow_resources.py, copied beside this file; cached for a few
|
|
20
|
+
seconds, so most prompts cost one small file read).
|
|
16
21
|
"""
|
|
17
22
|
from datetime import datetime, timezone
|
|
18
23
|
import hashlib
|
|
@@ -152,6 +157,53 @@ def context_line(mode):
|
|
|
152
157
|
"explicitamente neste pedido. Religar: /crow on ou crow-skills mode on.")
|
|
153
158
|
|
|
154
159
|
|
|
160
|
+
# --- machine resources ------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
_RESOURCES = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resources_module():
|
|
166
|
+
"""The resource probe: the package module, or crow_resources.py beside this file.
|
|
167
|
+
|
|
168
|
+
Loaded by path because the hooks run with ``python -I`` (the script's directory is
|
|
169
|
+
not on sys.path). None when it is missing: the hook then adds nothing.
|
|
170
|
+
"""
|
|
171
|
+
global _RESOURCES
|
|
172
|
+
if _RESOURCES is None:
|
|
173
|
+
_RESOURCES = False
|
|
174
|
+
try:
|
|
175
|
+
if __package__:
|
|
176
|
+
from . import resources as module # type: ignore[attr-defined]
|
|
177
|
+
_RESOURCES = module
|
|
178
|
+
else:
|
|
179
|
+
import importlib.util
|
|
180
|
+
here = Path(os.path.abspath(__file__)).parent
|
|
181
|
+
for name in ("crow_resources.py", "resources.py"):
|
|
182
|
+
path = here / name
|
|
183
|
+
if path.is_file() and not path.is_symlink():
|
|
184
|
+
spec = importlib.util.spec_from_file_location("crow_resources", path)
|
|
185
|
+
module = importlib.util.module_from_spec(spec)
|
|
186
|
+
# Bytecode is cached in __pycache__ beside it (~5 ms less per prompt);
|
|
187
|
+
# the integrations uninstall removes that directory with the scripts.
|
|
188
|
+
spec.loader.exec_module(module)
|
|
189
|
+
_RESOURCES = module
|
|
190
|
+
break
|
|
191
|
+
except Exception: # noqa: BLE001 - a broken probe never breaks the hook
|
|
192
|
+
_RESOURCES = False
|
|
193
|
+
return _RESOURCES or None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def resource_hint(home):
|
|
197
|
+
"""The one-line memory hint, or None (no pressure, no probe, or any failure)."""
|
|
198
|
+
module = resources_module()
|
|
199
|
+
if module is None:
|
|
200
|
+
return None
|
|
201
|
+
try:
|
|
202
|
+
return module.hint(module.probe(home, processes=False, use_cache=True))
|
|
203
|
+
except Exception: # noqa: BLE001
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
155
207
|
# --- event bus ----------------------------------------------------------------------------
|
|
156
208
|
|
|
157
209
|
def append_event(home, event):
|
|
@@ -472,7 +524,11 @@ def handle(host, raw, payload, home, env=None):
|
|
|
472
524
|
update_session(home, dict(stamped, event="other"), {"modeSeen": stamp, "mode": current["mode"]})
|
|
473
525
|
if raw == "SessionStart":
|
|
474
526
|
return output_for(host, raw, system=system) if system else None
|
|
475
|
-
|
|
527
|
+
context = context_line(current["mode"])
|
|
528
|
+
memory = resource_hint(home)
|
|
529
|
+
if memory:
|
|
530
|
+
context += "\n" + memory
|
|
531
|
+
return output_for(host, raw, context=context, system=system)
|
|
476
532
|
|
|
477
533
|
|
|
478
534
|
def update_metrics(home, host, payload):
|
|
@@ -55,6 +55,7 @@ OWNERS = {"claude-statusline": ("claude",), "claude-hooks": ("claude",), "claude
|
|
|
55
55
|
"gemini-hooks": ("gemini",), "gemini-command": ("gemini",), "agy-hooks": ("gemini",), "agy-statusline": ("gemini",),
|
|
56
56
|
"devin-hooks": ("devin",), "tmux-status": ("claude", "codex", "gemini", "devin")}
|
|
57
57
|
HOOK_SCRIPT = "crow_hook.py"
|
|
58
|
+
RESOURCES_SCRIPT = "crow_resources.py"
|
|
58
59
|
COMMAND_HEADER = "Gerenciado por crow-skills (comando /crow). Não edite; use crow-skills integrations."
|
|
59
60
|
BEGIN = "# >>> crow-skills integrations (gerenciado; use crow-skills integrations uninstall) >>>"
|
|
60
61
|
END = "# <<< crow-skills integrations <<<"
|
|
@@ -117,6 +118,37 @@ def hook_source():
|
|
|
117
118
|
return (Path(__file__).resolve().parent / "hook.py").read_bytes()
|
|
118
119
|
|
|
119
120
|
|
|
121
|
+
def resources_script_path(store):
|
|
122
|
+
return root(store) / RESOURCES_SCRIPT
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resources_source():
|
|
126
|
+
return (Path(__file__).resolve().parent / "resources.py").read_bytes()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def remove_bytecode(store):
|
|
130
|
+
"""__pycache__ beside the copied scripts (written when the status line imports crow_hook):
|
|
131
|
+
removed with them, only when it holds nothing but their bytecode."""
|
|
132
|
+
cache = root(store) / "__pycache__"
|
|
133
|
+
if cache.is_symlink() or not cache.is_dir():
|
|
134
|
+
return
|
|
135
|
+
names = ("crow_progress.", "crow_hook.", "crow_resources.")
|
|
136
|
+
entries = list(cache.iterdir())
|
|
137
|
+
if all(e.is_file() and not e.is_symlink() and e.name.startswith(names) and e.suffix == ".pyc" for e in entries):
|
|
138
|
+
try:
|
|
139
|
+
for entry in entries:
|
|
140
|
+
entry.unlink()
|
|
141
|
+
cache.rmdir()
|
|
142
|
+
except OSError:
|
|
143
|
+
pass # a hook wrote bytecode meanwhile: the directory stays, nothing else is touched
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def scripts(store):
|
|
147
|
+
"""(installed path, source bytes) of every script the hooks and status lines run."""
|
|
148
|
+
return ((script_path(store), script_source()), (hook_script_path(store), hook_source()),
|
|
149
|
+
(resources_script_path(store), resources_source()))
|
|
150
|
+
|
|
151
|
+
|
|
120
152
|
def quoted_ok(*paths):
|
|
121
153
|
return all(not re.search(r"[\"'\\$`\n]", str(p)) for p in paths)
|
|
122
154
|
|
|
@@ -825,7 +857,7 @@ def sync(store, home, wanted, dry_run=False, python=None):
|
|
|
825
857
|
with (nullcontext() if dry_run else store.lock()):
|
|
826
858
|
if not dry_run and any(set(OWNERS[n]) & set(wanted) for n in NAMES):
|
|
827
859
|
private_dir(root(store))
|
|
828
|
-
for script, source in (
|
|
860
|
+
for script, source in scripts(store):
|
|
829
861
|
if not script.exists() or sha(script.read_bytes()) != sha(source):
|
|
830
862
|
write_atomic(checked(script), source, 0o644)
|
|
831
863
|
# Removals first and in reverse order, then installs in order: two integrations that
|
|
@@ -859,9 +891,10 @@ def sync(store, home, wanted, dry_run=False, python=None):
|
|
|
859
891
|
if after != state:
|
|
860
892
|
atomic_json(state_path(store), after)
|
|
861
893
|
if not after["integrations"]:
|
|
862
|
-
for script in (script_path(store), hook_script_path(store)):
|
|
894
|
+
for script in (script_path(store), hook_script_path(store), resources_script_path(store)):
|
|
863
895
|
if script.exists():
|
|
864
896
|
script.unlink()
|
|
897
|
+
remove_bytecode(store)
|
|
865
898
|
for key in ("changes", "skipped"):
|
|
866
899
|
report[key].sort(key=lambda c: NAMES.index(c["integration"]) if c["integration"] in NAMES else 99)
|
|
867
900
|
report["dry_run"] = dry_run
|
package/crow_skills/progress.py
CHANGED
|
@@ -83,7 +83,7 @@ def validate(data):
|
|
|
83
83
|
"status": data.get("status") if data.get("status") in ("running",) + FINISHED else "running",
|
|
84
84
|
"startedAt": data.get("startedAt"), "updatedAt": data.get("updatedAt"), "endedAt": data.get("endedAt"),
|
|
85
85
|
"session": data.get("session") if isinstance(data.get("session"), dict) else {},
|
|
86
|
-
"phases": [], "units": []}
|
|
86
|
+
"phases": [], "units": [], "resources": resources_decision(data.get("resources"))}
|
|
87
87
|
for phase in phases:
|
|
88
88
|
if isinstance(phase, dict) and isinstance(phase.get("id"), str):
|
|
89
89
|
run["phases"].append({"id": phase["id"], "title": safe(phase.get("title") or phase["id"]),
|
|
@@ -107,6 +107,42 @@ def validate(data):
|
|
|
107
107
|
return run
|
|
108
108
|
|
|
109
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
|
+
|
|
110
146
|
def load(path):
|
|
111
147
|
path = Path(path)
|
|
112
148
|
if path.is_symlink() or not path.is_file() or path.stat().st_size > MAX_LEDGER:
|
|
@@ -515,6 +551,8 @@ def render(run, usage=None, now=None, color=False, unicode=True, width=20):
|
|
|
515
551
|
bar(info["progress"], width, unicode) + " " + str(int(info["progress"] * 100)).rjust(3) + "% " +
|
|
516
552
|
str(info["done"]) + "/" + str(info["total"]) + " unidades" +
|
|
517
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)))
|
|
518
556
|
lines += ["", *agent_rows(run, unicode, counts=False), ""]
|
|
519
557
|
phase_ids = [p["id"] for p in run["phases"]]
|
|
520
558
|
groups = [(p, [u for u in run["units"] if u["phase"] == p["id"]]) for p in run["phases"]]
|
|
@@ -649,6 +687,18 @@ def mode_text(home, unicode=True):
|
|
|
649
687
|
return ""
|
|
650
688
|
|
|
651
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
|
+
|
|
652
702
|
def statusline(home, stdin_text, chain=None, host="claude"):
|
|
653
703
|
"""Status line for Claude Code and Antigravity (agy).
|
|
654
704
|
|
|
@@ -684,7 +734,7 @@ def statusline(home, stdin_text, chain=None, host="claude"):
|
|
|
684
734
|
except (OSError, subprocess.SubprocessError):
|
|
685
735
|
previous = ""
|
|
686
736
|
run = select(discover(cwd, home), cwd, strict=True, session=session)
|
|
687
|
-
parts = [p for p in (mode_text(home),) if p]
|
|
737
|
+
parts = [p for p in (mode_text(home), memory_text(home)) if p]
|
|
688
738
|
rows = []
|
|
689
739
|
if active(run):
|
|
690
740
|
if run["host"] == "claude" and host == "claude" and not run["session"].get("id") and session:
|
|
@@ -711,12 +761,12 @@ def statusline(home, stdin_text, chain=None, host="claude"):
|
|
|
711
761
|
|
|
712
762
|
|
|
713
763
|
def tmux_segment(home, cwd):
|
|
714
|
-
|
|
764
|
+
head = " · ".join(p for p in (mode_text(home), memory_text(home)) if p)
|
|
715
765
|
run = select(discover(cwd, home), cwd, strict=True)
|
|
716
766
|
if not active(run):
|
|
717
|
-
return
|
|
767
|
+
return head.replace("#", "##")
|
|
718
768
|
text = compact(run, tokens(run, home), unicode=True, width=8)
|
|
719
|
-
return ((
|
|
769
|
+
return ((head + " · " if head else "") + text).replace("#", "##")
|
|
720
770
|
|
|
721
771
|
|
|
722
772
|
def main(argv=None):
|