@andespindola/crow-skills 0.10.0 → 0.10.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/README.md +2 -2
- package/crow_skills/__init__.py +1 -1
- package/crow_skills/cli.py +4 -0
- package/crow_skills/events.py +5 -2
- package/crow_skills/integrations.py +219 -3
- package/crow_skills/resources.py +326 -102
- package/docs/cli.md +28 -14
- package/package.json +1 -1
- package/release.json +14 -14
- package/skills/crow-behaviour/SKILL.md +2 -1
- package/skills/crow-behaviour/references/progress-ledger.md +1 -1
- package/skills/crow-behaviour/references/resources.md +25 -5
- package/skills/crow-behaviour/scripts/crow_ledger.py +7 -1
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.10.
|
|
5
|
+
**Versão 0.10.1** ([mudanças](CHANGELOG.md)): o controle de recursos passa a tratar swap e CPU como eixos de pressão (swap quase cheio e carga alta reduzem agentes, jobs e workers; nenhuma recomendação cresce com a pressão), mostra a CPU% dos CLIs de agente, traz `crow-skills resources --exec-prefix` para rodar jobs pesados em prioridade baixa e as regras da crow-behaviour sob pressão (reduzir já, um job pesado por vez, não repetir verificação, validação ampla no CI); os hooks do Codex vão para o `config.toml` quando o `hooks.json` é symlink. A 0.10.0 trouxe o crow-broadcast, que compartilha, de forma passiva, o que cada sessão e cada CLI (Claude Code, Codex, Gemini CLI, Antigravity, Devin, inclusive dentro do Uniflow) está fazendo nesta máquina: repositório, clone, branch, execução crow em andamento e arquivos em edição; o prompt recebe um resumo só quando outra sessão viva está no mesmo repositório, editar um arquivo que outra sessão tocou ou trocar de branch num clone em uso gera aviso (ou bloqueio, se configurado), e `crow-skills broadcast claim` reserva áreas; a crow-behaviour consulta isso antes de planejar e reserva as áreas que delega ([crow-broadcast](docs/broadcast.md)). A 0.9.0 trouxe o controle de 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.10.
|
|
33
|
+
npm install -g @andespindola/crow-skills@0.10.1
|
|
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
|
@@ -96,6 +96,8 @@ def parser():
|
|
|
96
96
|
child = sub.add_parser("resources", parents=[common],
|
|
97
97
|
help="Memória, pressão, agentes em execução e paralelismo recomendado (somente leitura)")
|
|
98
98
|
child.add_argument("--no-processes", action="store_true", help="Não varrer os processos dos CLIs de agente (mais rápido)")
|
|
99
|
+
child.add_argument("--exec-prefix", action="store_true",
|
|
100
|
+
help="Só imprimir o prefixo que roda um job pesado em prioridade baixa neste sistema (taskpolicy/nice/ionice/start /low)")
|
|
99
101
|
child = sub.add_parser("autoupdate", parents=[common], help="Atualização diária e no login do crow-skills e das skills gerenciadas")
|
|
100
102
|
child.add_argument("action", choices=("on", "off", "status", "run"))
|
|
101
103
|
child.add_argument("--git-pull", action="store_true", help="on: também atualizar um checkout git limpo em main/master (git pull --ff-only)")
|
|
@@ -622,6 +624,8 @@ def run(args):
|
|
|
622
624
|
if args.command == "broadcast":
|
|
623
625
|
return broadcast_command(args, home)
|
|
624
626
|
if args.command == "resources":
|
|
627
|
+
if args.exec_prefix:
|
|
628
|
+
return resources.exec_prefix_data()
|
|
625
629
|
data = resources.probe(home, processes=not args.no_processes)
|
|
626
630
|
data["text"] = resources.render(data)
|
|
627
631
|
return data
|
package/crow_skills/events.py
CHANGED
|
@@ -100,11 +100,14 @@ def resources_field(home):
|
|
|
100
100
|
"""crow-resources/1 subset for terminals: memory, pressure and the recommendation.
|
|
101
101
|
|
|
102
102
|
Added in 0.9.0 without changing crow-snapshot/1: consumers that do not know the
|
|
103
|
-
field ignore it; the field carries its own schema for future changes.
|
|
103
|
+
field ignore it; the field carries its own schema for future changes. 0.10.1 adds
|
|
104
|
+
pressure.swap and cpu (additive).
|
|
104
105
|
"""
|
|
105
106
|
data = resources.probe(home, processes=False, use_cache=True)
|
|
106
107
|
return {"schema": data["schema"], "generatedAt": data["generatedAt"], "cached": data["cached"],
|
|
107
|
-
"memory": data["memory"], "pressure": {"level": data["pressure"]["level"], "reasons": data["pressure"]["reasons"]
|
|
108
|
+
"memory": data["memory"], "pressure": {"level": data["pressure"]["level"], "reasons": data["pressure"]["reasons"],
|
|
109
|
+
"swap": data["pressure"].get("swap")},
|
|
110
|
+
"cpu": {k: data["cpu"].get(k) for k in ("count", "load1", "load5", "load15", "loadPerCpu1", "loadPerCpu5", "level", "reasons")},
|
|
108
111
|
"recommendation": data["recommendation"], "alert": resources.alert(data), "segment": resources.segment(data)}
|
|
109
112
|
|
|
110
113
|
|
|
@@ -60,6 +60,9 @@ BROADCAST_SCRIPT = "crow_broadcast.py"
|
|
|
60
60
|
COMMAND_HEADER = "Gerenciado por crow-skills (comando /crow). Não edite; use crow-skills integrations."
|
|
61
61
|
BEGIN = "# >>> crow-skills integrations (gerenciado; use crow-skills integrations uninstall) >>>"
|
|
62
62
|
END = "# <<< crow-skills integrations <<<"
|
|
63
|
+
# codex-hooks inside ~/.codex/config.toml, used when ~/.codex/hooks.json is a symlink (a dotfiles repo) or unsafe.
|
|
64
|
+
HOOKS_BEGIN = "# >>> crow-skills codex-hooks (gerenciado; use crow-skills integrations uninstall) >>>"
|
|
65
|
+
HOOKS_END = "# <<< crow-skills codex-hooks <<<"
|
|
63
66
|
CODEX_ITEMS = ["model-with-reasoning", "context-remaining", "current-dir", "task-progress", "used-tokens"]
|
|
64
67
|
SNIPPET_HEADER = "# Gerenciado por crow-skills (integração tmux). Não edite; use crow-skills integrations."
|
|
65
68
|
UNSUPPORTED = {
|
|
@@ -494,6 +497,24 @@ def add_hooks(data, entries, layout):
|
|
|
494
497
|
|
|
495
498
|
|
|
496
499
|
def hooks_sync(name, store, home, record, desired, python, dry):
|
|
500
|
+
if name == "codex-hooks":
|
|
501
|
+
unsafe = codex_hooks_json_unsafe(home)
|
|
502
|
+
if record and record.get("mode") != "toml" and unsafe:
|
|
503
|
+
# Installed into a regular hooks.json that has since become a symlink (moved into dotfiles):
|
|
504
|
+
# our entries live in the target now. Keep the record (and the script they call) until the
|
|
505
|
+
# user removes them; then the hooks move to config.toml on the next install.
|
|
506
|
+
left = codex_json_duplicates(home)
|
|
507
|
+
if left:
|
|
508
|
+
return record, "skip", ("~/.codex/hooks.json virou symlink (ou ficou inseguro) depois que o crow-skills gravou nele: "
|
|
509
|
+
+ str(left) + " entrada(s) do crow-skills com crow_hook.py continuam no arquivo de destino, "
|
|
510
|
+
"que o crow-skills não edita. Remova-as de lá" + (" e rode crow-skills integrations install de novo: os hooks "
|
|
511
|
+
"passam para ~/.codex/config.toml." if desired else "; o registro e o script que elas chamam ficam "
|
|
512
|
+
"até isso, para o Codex não chamar um script apagado."))
|
|
513
|
+
record = None
|
|
514
|
+
if not desired:
|
|
515
|
+
return None, "skip", "Entradas do crow-skills já ausentes de ~/.codex/hooks.json; controle encerrado."
|
|
516
|
+
if (record or {}).get("mode") == "toml" or (unsafe and (desired or not record)):
|
|
517
|
+
return codex_hooks_toml_sync(store, home, record, desired, python, dry, unsafe)
|
|
497
518
|
spec = HOOKS[name]
|
|
498
519
|
target = spec["file"](home)
|
|
499
520
|
if desired and not record and spec["requires"] is not None and not os.path.exists(spec["requires"](home)):
|
|
@@ -535,6 +556,192 @@ def hooks_sync(name, store, home, record, desired, python, dry):
|
|
|
535
556
|
return new_record, action, note
|
|
536
557
|
|
|
537
558
|
|
|
559
|
+
# --- codex-hooks in config.toml ------------------------------------------------------------------
|
|
560
|
+
# Codex 0.165 reads hooks from hooks.json and from config.toml (HooksToml: one array of matcher
|
|
561
|
+
# groups per event, each {matcher?, hooks: [{type = "command", command, timeout, async, ...}]};
|
|
562
|
+
# the same schema as hooks.json, read from the binary). When ~/.codex/hooks.json is a symlink
|
|
563
|
+
# (typically into a dotfiles repository) crow-skills never writes through it: the entries go to a
|
|
564
|
+
# marked block at the end of config.toml, verified by parsing and restored byte for byte.
|
|
565
|
+
|
|
566
|
+
def codex_hooks_json_unsafe(home):
|
|
567
|
+
"""Why ~/.codex/hooks.json cannot be edited safely ("symlink" or a reason), or None."""
|
|
568
|
+
path = home / ".codex" / "hooks.json"
|
|
569
|
+
if os.path.islink(path):
|
|
570
|
+
return "symlink"
|
|
571
|
+
try:
|
|
572
|
+
read_regular(protect(path, home))
|
|
573
|
+
except CrowError as exc:
|
|
574
|
+
return str(exc)
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def codex_json_duplicates(home):
|
|
579
|
+
"""crow_hook.py entries in ~/.codex/hooks.json when crow-skills does not manage that file
|
|
580
|
+
(read only, through the symlink): with the config.toml block they would run twice."""
|
|
581
|
+
path = home / ".codex" / "hooks.json"
|
|
582
|
+
try:
|
|
583
|
+
if not os.path.exists(path) or os.path.getsize(path) > 1024 * 1024:
|
|
584
|
+
return 0
|
|
585
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
586
|
+
data = json.load(handle)
|
|
587
|
+
except (OSError, ValueError):
|
|
588
|
+
return 0
|
|
589
|
+
count = 0
|
|
590
|
+
hooks = data.get("hooks") if isinstance(data, dict) else None
|
|
591
|
+
for groups in (hooks.values() if isinstance(hooks, dict) else ()):
|
|
592
|
+
for group in groups if isinstance(groups, list) else []:
|
|
593
|
+
items = group.get("hooks") if isinstance(group, dict) else None
|
|
594
|
+
for item in items if isinstance(items, list) else []:
|
|
595
|
+
if isinstance(item, dict) and HOOK_SCRIPT in str(item.get("command", "")):
|
|
596
|
+
count += 1
|
|
597
|
+
return count
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def toml_string(value):
|
|
601
|
+
return json.dumps(value, ensure_ascii=False) # a JSON string is a valid TOML basic string
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def codex_hooks_block(entries):
|
|
605
|
+
lines = [HOOKS_BEGIN]
|
|
606
|
+
for event, groups in entries.items():
|
|
607
|
+
for group in groups:
|
|
608
|
+
lines.append("[[hooks." + event + "]]")
|
|
609
|
+
if "matcher" in group:
|
|
610
|
+
lines.append("matcher = " + toml_string(group["matcher"]))
|
|
611
|
+
for item in group["hooks"]:
|
|
612
|
+
lines.append("[[hooks." + event + ".hooks]]")
|
|
613
|
+
for key, value in item.items():
|
|
614
|
+
lines.append(key + " = " + (("true" if value else "false") if isinstance(value, bool) else
|
|
615
|
+
str(value) if isinstance(value, int) else toml_string(value)))
|
|
616
|
+
return "\n".join(lines + [HOOKS_END]) + "\n"
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def without_groups(parsed, groups):
|
|
620
|
+
"""parsed minus each of groups ({event: [group]}), each removed once by equality; None when
|
|
621
|
+
one of them is not there unchanged (edited, or keys appended to it)."""
|
|
622
|
+
result = deepcopy(parsed)
|
|
623
|
+
hooks = result.get("hooks")
|
|
624
|
+
for event, wanted in groups.items():
|
|
625
|
+
current = hooks.get(event) if isinstance(hooks, dict) else None
|
|
626
|
+
if not isinstance(current, list):
|
|
627
|
+
return None
|
|
628
|
+
for group in wanted:
|
|
629
|
+
if group not in current:
|
|
630
|
+
return None
|
|
631
|
+
del current[len(current) - 1 - current[::-1].index(group)] # the last equal one: ours come after the user's
|
|
632
|
+
if not current:
|
|
633
|
+
del hooks[event]
|
|
634
|
+
return result
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def codex_hooks_toml_sync(store, home, record, desired, python, dry, unsafe):
|
|
638
|
+
label = "~/.codex/config.toml"
|
|
639
|
+
if tomllib is None:
|
|
640
|
+
return record, "skip", "Python 3.11+ (tomllib) é necessário para gravar os hooks do Codex em config.toml com verificação."
|
|
641
|
+
directory = home / ".codex"
|
|
642
|
+
if desired and not record and not directory.is_dir():
|
|
643
|
+
return None, "skip", "~/.codex/hooks.json: CLI não configurado neste perfil; nada alterado."
|
|
644
|
+
path = protect(directory / "config.toml", home)
|
|
645
|
+
old = read_regular(path)
|
|
646
|
+
try:
|
|
647
|
+
text = old.decode("utf-8") if old is not None else ""
|
|
648
|
+
except UnicodeDecodeError:
|
|
649
|
+
return record, "skip", "config.toml não é UTF-8; hooks do Codex não configurados."
|
|
650
|
+
span = marked(text, HOOKS_BEGIN, HOOKS_END)
|
|
651
|
+
stripped = text[:span[0]] + text[span[1]:] if span else text
|
|
652
|
+
try:
|
|
653
|
+
current = tomllib.loads(text)
|
|
654
|
+
base = tomllib.loads(stripped)
|
|
655
|
+
except tomllib.TOMLDecodeError:
|
|
656
|
+
return record, "skip", "config.toml não é TOML válido; hooks do Codex não configurados."
|
|
657
|
+
old_block = text[span[0]:span[1]] if span else None
|
|
658
|
+
if span and (not record or record.get("block_sha") != sha(old_block.encode("utf-8"))):
|
|
659
|
+
return (record if desired else None), "skip", ("Bloco codex-hooks do crow-skills em config.toml foi editado ou não tem registro; "
|
|
660
|
+
"nada alterado. Remova o bloco manualmente e rode crow-skills integrations install.")
|
|
661
|
+
if span:
|
|
662
|
+
old_groups = tomllib.loads(old_block).get("hooks", {})
|
|
663
|
+
expected_base = without_groups(current, old_groups)
|
|
664
|
+
if expected_base is not None and "hooks" in expected_base and not expected_base["hooks"] and "hooks" not in base:
|
|
665
|
+
expected_base.pop("hooks")
|
|
666
|
+
if expected_base != base:
|
|
667
|
+
return (record if desired else None), "skip", ("Chaves acrescentadas logo depois do bloco codex-hooks em config.toml mudariam de "
|
|
668
|
+
"tabela; nada alterado. Revise o arquivo manualmente.")
|
|
669
|
+
added_newline = (record or {}).get("added_newline", False)
|
|
670
|
+
if not desired:
|
|
671
|
+
if not record:
|
|
672
|
+
return None, "unchanged", None
|
|
673
|
+
if not span:
|
|
674
|
+
return None, "skip", "Bloco codex-hooks ausente de config.toml; controle encerrado."
|
|
675
|
+
result = stripped
|
|
676
|
+
if added_newline and span[1] == len(text) and result.endswith("\n"):
|
|
677
|
+
result = result[:-1]
|
|
678
|
+
if dry:
|
|
679
|
+
return None, "remove", None
|
|
680
|
+
backup(store, "codex-config", old)
|
|
681
|
+
if record.get("created_file") and not result.strip():
|
|
682
|
+
path.unlink()
|
|
683
|
+
else:
|
|
684
|
+
write(path, result.encode("utf-8"), old)
|
|
685
|
+
return None, "remove", None
|
|
686
|
+
if not quoted_ok(python, hook_script_path(store), home):
|
|
687
|
+
return record, "skip", "Caminho do Python, do estado ou do home contém aspas/variáveis; hooks não configurados."
|
|
688
|
+
entries = hook_entries(store, home, python, "codex")
|
|
689
|
+
block = codex_hooks_block(entries)
|
|
690
|
+
if span:
|
|
691
|
+
if old_block == block:
|
|
692
|
+
report_duplicates(home)
|
|
693
|
+
return record, "unchanged", None
|
|
694
|
+
result = text[:span[0]] + block + text[span[1]:]
|
|
695
|
+
else:
|
|
696
|
+
added_newline = bool(stripped) and not stripped.endswith("\n")
|
|
697
|
+
result = stripped + ("\n" if added_newline else "") + block
|
|
698
|
+
expected = deepcopy(base)
|
|
699
|
+
hooks = expected.setdefault("hooks", {})
|
|
700
|
+
try:
|
|
701
|
+
for event, groups in entries.items():
|
|
702
|
+
if not isinstance(hooks.setdefault(event, []), list):
|
|
703
|
+
raise ValueError(event)
|
|
704
|
+
hooks[event].extend(deepcopy(groups))
|
|
705
|
+
if tomllib.loads(result) != expected:
|
|
706
|
+
raise ValueError("diff")
|
|
707
|
+
except (tomllib.TOMLDecodeError, ValueError, AttributeError):
|
|
708
|
+
return record, "skip", ("A tabela [hooks] de config.toml tem um formato que o bloco do crow-skills não preservaria "
|
|
709
|
+
"(por exemplo, hooks definidos inline); nada alterado.")
|
|
710
|
+
action = "update" if record else "install"
|
|
711
|
+
new_record = {"path": str(path), "mode": "toml", "added_newline": added_newline, "installed_at": now(),
|
|
712
|
+
"block_sha": sha(block.encode("utf-8")),
|
|
713
|
+
"created_file": record.get("created_file", False) if record and record.get("mode") == "toml" else old is None}
|
|
714
|
+
note = ("~/.codex/hooks.json " + ("é symlink" if unsafe == "symlink" else "não pode ser editado com segurança") +
|
|
715
|
+
"; os hooks do crow-skills foram gravados em ~/.codex/config.toml (bloco marcado) e o hooks.json não foi tocado. "
|
|
716
|
+
"Codex só executa hook novo ou alterado depois de aprovado: abra o Codex, rode /hooks e aprove as entradas do "
|
|
717
|
+
"crow-skills.") + codex_hooks_feature(home)
|
|
718
|
+
report_duplicates(home)
|
|
719
|
+
if dry:
|
|
720
|
+
return new_record, action, note.strip()
|
|
721
|
+
new_record["backup"] = backup(store, "codex-config", old)
|
|
722
|
+
write(path, result.encode("utf-8"), old)
|
|
723
|
+
return new_record, action, note.strip()
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# Findings that do not stop an integration but need the user (reported in "skipped").
|
|
727
|
+
_WARNINGS = []
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def report_duplicates(home):
|
|
731
|
+
note = codex_duplicates_note(home)
|
|
732
|
+
if note:
|
|
733
|
+
_WARNINGS.append({"integration": "codex-hooks", "reason": note})
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def codex_duplicates_note(home):
|
|
737
|
+
count = codex_json_duplicates(home)
|
|
738
|
+
if not count:
|
|
739
|
+
return None
|
|
740
|
+
return ("~/.codex/hooks.json (não gerenciado pelo crow-skills) já tem " + str(count) +
|
|
741
|
+
" entrada(s) com crow_hook.py, provavelmente adicionadas à mão; com o bloco em config.toml elas rodariam em dobro. "
|
|
742
|
+
"Remova-as do hooks.json (ou do arquivo para onde o symlink aponta); o crow-skills não edita esse arquivo.")
|
|
743
|
+
|
|
744
|
+
|
|
538
745
|
# --- /crow slash commands ------------------------------------------------------------------------
|
|
539
746
|
|
|
540
747
|
COMMANDS = {
|
|
@@ -612,9 +819,9 @@ def command_sync(name, store, home, record, desired, python, dry):
|
|
|
612
819
|
|
|
613
820
|
# --- codex ---------------------------------------------------------------------------------
|
|
614
821
|
|
|
615
|
-
def marked(text):
|
|
616
|
-
begins = [m.start() for m in re.finditer(r"(?m)^" + re.escape(
|
|
617
|
-
ends = [m.end() for m in re.finditer(r"(?m)^" + re.escape(
|
|
822
|
+
def marked(text, begin=BEGIN, end=END):
|
|
823
|
+
begins = [m.start() for m in re.finditer(r"(?m)^" + re.escape(begin) + r"\n", text)]
|
|
824
|
+
ends = [m.end() for m in re.finditer(r"(?m)^" + re.escape(end) + r"\n", text)]
|
|
618
825
|
if not begins and not ends:
|
|
619
826
|
return None
|
|
620
827
|
if len(begins) != 1 or len(ends) != 1 or ends[0] <= begins[0]:
|
|
@@ -886,6 +1093,7 @@ def sync(store, home, wanted, dry_run=False, python=None):
|
|
|
886
1093
|
after["files"][record["path"]] = {"backup": record.get("backup"), "created_file": record.get("created_file", False),
|
|
887
1094
|
"written_sha": record["written_sha"]}
|
|
888
1095
|
global _FILES, _LIVE
|
|
1096
|
+
del _WARNINGS[:]
|
|
889
1097
|
_FILES = {k: dict(v) for k, v in after["files"].items()}
|
|
890
1098
|
_LIVE = after["integrations"]
|
|
891
1099
|
with (nullcontext() if dry_run else store.lock()):
|
|
@@ -930,6 +1138,8 @@ def sync(store, home, wanted, dry_run=False, python=None):
|
|
|
930
1138
|
if script.exists():
|
|
931
1139
|
script.unlink()
|
|
932
1140
|
remove_bytecode(store)
|
|
1141
|
+
report["skipped"].extend(_WARNINGS)
|
|
1142
|
+
del _WARNINGS[:]
|
|
933
1143
|
for key in ("changes", "skipped"):
|
|
934
1144
|
report[key].sort(key=lambda c: NAMES.index(c["integration"]) if c["integration"] in NAMES else 99)
|
|
935
1145
|
report["dry_run"] = dry_run
|
|
@@ -946,6 +1156,12 @@ def status(store, home):
|
|
|
946
1156
|
row["path"] = record.get("path") or record.get("conf")
|
|
947
1157
|
if name == "tmux-status" and not record.get("block"):
|
|
948
1158
|
row["note"] = "tmux.conf é symlink; linha source-file adicionada manualmente pelo usuário."
|
|
1159
|
+
if name == "codex-hooks" and record.get("mode") == "toml":
|
|
1160
|
+
row["note"] = "Gravado em ~/.codex/config.toml (bloco marcado): ~/.codex/hooks.json é symlink ou inseguro e não é editado."
|
|
1161
|
+
duplicates = codex_duplicates_note(home)
|
|
1162
|
+
if duplicates:
|
|
1163
|
+
row["note"] += " " + duplicates
|
|
1164
|
+
row["duplicates"] = codex_json_duplicates(home)
|
|
949
1165
|
rows.append(row)
|
|
950
1166
|
for agent, reason in UNSUPPORTED.items():
|
|
951
1167
|
rows.append({"integration": UNSUPPORTED_ROW[agent], "agents": [agent], "status": "unsupported", "note": reason})
|