@andespindola/crow-skills 0.18.2 → 0.19.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
CHANGED
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
|
|
@@ -905,6 +921,49 @@ def repo_scope(snap, now=None, window=None):
|
|
|
905
921
|
return ids, roots
|
|
906
922
|
|
|
907
923
|
|
|
924
|
+
|
|
925
|
+
def occupancy(snap, now=None, window=None):
|
|
926
|
+
"""The clones a session OCCUPIES, as opposed to the ones it merely reached.
|
|
927
|
+
|
|
928
|
+
Occupied: the cwd clone, a clone where it edited a file, and a clone a git command actually
|
|
929
|
+
drove (``driven``). Reached-only is a clone a shell call read or cd'ed into — and conflating the
|
|
930
|
+
two has a measured cost: a read-only ``cd`` plus a ``sed`` of one doc marked a session as using
|
|
931
|
+
a clone, which made a peer session stop a running test suite and apologise for damage to work
|
|
932
|
+
that did not exist there. Reach stays visible as information; only occupancy warns.
|
|
933
|
+
"""
|
|
934
|
+
now = now or time.time()
|
|
935
|
+
window = FILE_TTL if window is None else window
|
|
936
|
+
ids, roots = set(), set()
|
|
937
|
+
presence = presence_of(snap)
|
|
938
|
+
repo = snap.get("repo") if isinstance(snap.get("repo"), dict) else None
|
|
939
|
+
if repo is None:
|
|
940
|
+
repo = presence.get("repo") if isinstance(presence.get("repo"), dict) else None
|
|
941
|
+
if repo is None and snap.get("cwd"):
|
|
942
|
+
repo = repo_info(snap["cwd"])
|
|
943
|
+
if isinstance(repo, dict): # where the session lives
|
|
944
|
+
if repo.get("id"):
|
|
945
|
+
ids.add(repo["id"])
|
|
946
|
+
if repo.get("root"):
|
|
947
|
+
roots.add(repo["root"])
|
|
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"):
|
|
950
|
+
continue
|
|
951
|
+
if now - (parse_time(entry.get("ts")) or 0) > window:
|
|
952
|
+
continue
|
|
953
|
+
if isinstance(entry.get("id"), str):
|
|
954
|
+
ids.add(entry["id"])
|
|
955
|
+
if isinstance(entry.get("root"), str):
|
|
956
|
+
roots.add(entry["root"])
|
|
957
|
+
for item in recent_files(snap, now, window): # and the clones whose files it edited
|
|
958
|
+
if isinstance(item.get("repo"), str):
|
|
959
|
+
ids.add(item["repo"])
|
|
960
|
+
rel, path = item.get("rel"), item.get("path")
|
|
961
|
+
if isinstance(rel, str) and rel and isinstance(path, str) and path.endswith(rel):
|
|
962
|
+
root = path[: len(path) - len(rel)].rstrip("/")
|
|
963
|
+
if root:
|
|
964
|
+
roots.add(root)
|
|
965
|
+
return ids, roots
|
|
966
|
+
|
|
908
967
|
def same_file(a, b):
|
|
909
968
|
"""Two touched-file entries name the same file: same path, or same repo id and repo-relative path."""
|
|
910
969
|
if a.get("path") == b.get("path"):
|
|
@@ -1492,8 +1551,14 @@ def relations(me, other, now, cfg):
|
|
|
1492
1551
|
my_ids, my_roots = repo_scope(me, now)
|
|
1493
1552
|
their_ids, their_roots = repo_scope(other, now)
|
|
1494
1553
|
shared_roots = sorted(my_roots & their_roots)
|
|
1554
|
+
# Reach and occupancy are different claims about the same clone: `sharedClones` is "both sides
|
|
1555
|
+
# touched it", `occupiedClones` is "both sides are WORKING in it". Only the second warns.
|
|
1556
|
+
my_live, their_live = occupancy(me, now), occupancy(other, now)
|
|
1557
|
+
occupied_roots = sorted(my_live[1] & their_live[1])
|
|
1558
|
+
occupied_ids = sorted(my_live[0] & their_live[0])
|
|
1495
1559
|
rel = {"sameRepo": bool(my_ids & their_ids), "sameClone": bool(shared_roots),
|
|
1496
1560
|
"sharedRepos": sorted(my_ids & their_ids), "sharedClones": shared_roots,
|
|
1561
|
+
"occupiedClones": occupied_roots, "occupiedRepos": occupied_ids,
|
|
1497
1562
|
"sameBranch": False, "overlap": []}
|
|
1498
1563
|
rel["sameBranch"] = bool(mine.get("id") and mine.get("id") == theirs.get("id") and me.get("branch")
|
|
1499
1564
|
and me.get("branch") == other.get("branch"))
|
|
@@ -1569,7 +1634,11 @@ def session_lines(s, home, now, with_files=True):
|
|
|
1569
1634
|
state = "bloqueada" if s.get("blocked") else {"working": "trabalhando", "idle": "ociosa", "blocked": "bloqueada",
|
|
1570
1635
|
"error": "com erro"}.get(s.get("state"), clean(s.get("state") or "?", 12))
|
|
1571
1636
|
root = (s.get("repo") or {}).get("root") or s.get("cwd") or "?"
|
|
1572
|
-
|
|
1637
|
+
occupied = rel.get("occupiedClones")
|
|
1638
|
+
occupied = occupied if isinstance(occupied, list) else (rel.get("sharedClones") or [])
|
|
1639
|
+
where = ("MESMO clone " if root in occupied
|
|
1640
|
+
else "leu/rodou comando em " if root in (rel.get("sharedClones") or [])
|
|
1641
|
+
else "clone ") + short_path(root)
|
|
1573
1642
|
head = "- " + clean(s.get("host"), 12) + " " + state + " · branch " + clean(s.get("branch") or "?", 60) + \
|
|
1574
1643
|
(" (a mesma)" if rel.get("sameBranch") else "") + " · " + where + " · " + age_text(s.get("ageSeconds"))
|
|
1575
1644
|
lines = [head[:LINE_CHARS]]
|
|
@@ -1584,10 +1653,18 @@ def session_lines(s, home, now, with_files=True):
|
|
|
1584
1653
|
names = [clean(f["rel"], 80) if f.get("rel") else short_path(f["path"], 60) for f in reversed(files)]
|
|
1585
1654
|
shown = ", ".join(names[:4]) + (" (+" + str(len(names) - 4) + ")" if len(names) > 4 else "")
|
|
1586
1655
|
lines.append((" arquivos editados: " + shown)[:LINE_CHARS])
|
|
1587
|
-
|
|
1588
|
-
if
|
|
1656
|
+
mine_id = (s.get("repo") or {}).get("id")
|
|
1657
|
+
shared = [r for r in (rel.get("sharedRepos") or []) if r != mine_id]
|
|
1658
|
+
# "trabalha em" was claiming too much: a read-only visit landed here too, and a peer session
|
|
1659
|
+
# stopped a running suite over it. The two claims are now separate lines.
|
|
1660
|
+
busy = [r for r in shared if r in (rel.get("occupiedRepos") or [])]
|
|
1661
|
+
read = [r for r in shared if r not in (rel.get("occupiedRepos") or [])]
|
|
1662
|
+
if busy:
|
|
1589
1663
|
lines.append((" TAMBÉM trabalha em repositório(s) deste trabalho: " +
|
|
1590
|
-
", ".join(clean(r, 60) for r in
|
|
1664
|
+
", ".join(clean(r, 60) for r in busy[:3]))[:LINE_CHARS])
|
|
1665
|
+
if read:
|
|
1666
|
+
lines.append((" leu/rodou comando em repositório(s) deste trabalho (sem editar): " +
|
|
1667
|
+
", ".join(clean(r, 60) for r in read[:3]))[:LINE_CHARS])
|
|
1591
1668
|
if rel.get("overlap"):
|
|
1592
1669
|
lines.append((" SOBREPOSIÇÃO com arquivos desta sessão: " + ", ".join(clean(x, 80) for x in rel["overlap"][:4]))[:LINE_CHARS])
|
|
1593
1670
|
return lines
|
|
@@ -1660,7 +1737,7 @@ def digest(home, me_key, me=None, now=None, cfg=None, deliver=True):
|
|
|
1660
1737
|
lines.append(claim_line(claim, live_by_key, now))
|
|
1661
1738
|
my_root = (view["me"].get("repo") or {}).get("root")
|
|
1662
1739
|
tail = []
|
|
1663
|
-
if my_root and any(my_root in ((s.get("relation") or {}).get("
|
|
1740
|
+
if my_root and any(my_root in ((s.get("relation") or {}).get("occupiedClones") or []) for s in others):
|
|
1664
1741
|
tail.append("ATENÇÃO: outra sessão viva trabalha neste mesmo clone (cwd, edição ou comando); "
|
|
1665
1742
|
"não faça checkout/switch/rebase/stash aqui.")
|
|
1666
1743
|
tail.append(FOOTER)
|
|
@@ -1710,7 +1787,7 @@ def check(home, me_key, me, paths=(), tree_change=(), now=None, cfg=None):
|
|
|
1710
1787
|
warnings.append("- " + clean(", ".join(hits[:4]), 160) + ": editado " + age_text(s.get("ageSeconds")) + " por " +
|
|
1711
1788
|
clean(s.get("host"), 12) + " (branch " + clean(s.get("branch") or "?", 60) + ", " +
|
|
1712
1789
|
short_path((s.get("repo") or {}).get("root") or s.get("cwd") or "?") + ")")
|
|
1713
|
-
theirs_root = next((r for r in sorted(
|
|
1790
|
+
theirs_root = next((r for r in sorted(occupancy(s, now)[1]) if r in tree_roots), None)
|
|
1714
1791
|
if theirs_root:
|
|
1715
1792
|
warnings.append("- o clone " + short_path(theirs_root) + " está em uso por " + clean(s.get("host"), 12) +
|
|
1716
1793
|
" na branch " + clean(s.get("branch") or "?", 60) + "; trocar de branch, rebase, stash ou reset aqui "
|
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": "0fac3e997aef5911a1a83e61ec8abc3b92abc0e02cdff187a430b3277e40b80f"
|
|
22
22
|
},
|
|
23
23
|
"crow_skills/__main__.py": {
|
|
24
24
|
"executable": false,
|
|
@@ -38,7 +38,7 @@
|
|
|
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,
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"package.json": {
|
|
129
129
|
"executable": false,
|
|
130
130
|
"kind": "file",
|
|
131
|
-
"sha256": "
|
|
131
|
+
"sha256": "25d9d44e380c13b64264f33b18d0b722a1b465c693fd92d819581b78df7f6fb8"
|
|
132
132
|
},
|
|
133
133
|
"skills/crow-accessibility/SKILL.md": {
|
|
134
134
|
"executable": false,
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"skills/crow-behaviour/references/broadcast.md": {
|
|
174
174
|
"executable": false,
|
|
175
175
|
"kind": "file",
|
|
176
|
-
"sha256": "
|
|
176
|
+
"sha256": "6653d4495b49856e2e8299dcb048f8b7e5b785a741367e6d25350e1f9aeb1cb3"
|
|
177
177
|
},
|
|
178
178
|
"skills/crow-behaviour/references/context-economy.md": {
|
|
179
179
|
"executable": false,
|
|
@@ -721,7 +721,7 @@
|
|
|
721
721
|
"sha256": "e37132ef8b060548191fbf14a5a18a9dd3acd84ea9a35e802174455b9e374d3d"
|
|
722
722
|
}
|
|
723
723
|
},
|
|
724
|
-
"revision": "
|
|
724
|
+
"revision": "2a8fb8a28c5b1073fd5faea2da0e2b4fe631fab7",
|
|
725
725
|
"schema_version": 1,
|
|
726
|
-
"version": "0.
|
|
726
|
+
"version": "0.19.1"
|
|
727
727
|
}
|
|
@@ -37,6 +37,15 @@ crow-skills broadcast release --all # ao concluir ou cancelar (só as des
|
|
|
37
37
|
- Libere ao terminar cada fase ou a execução. Reservas expiram pelo TTL (máximo 24 h) e somem quando a sessão dona deixa de estar viva.
|
|
38
38
|
- Sem outras sessões vivas no repositório, não reserve: é ruído.
|
|
39
39
|
|
|
40
|
+
## Alcance não é ocupação
|
|
41
|
+
|
|
42
|
+
O bloco distingue duas alegações sobre o mesmo clone, e só uma é motivo para parar:
|
|
43
|
+
|
|
44
|
+
- **`MESMO clone` / "TAMBÉM trabalha em"** — a outra sessão **ocupa** o clone: é o cwd dela, ela editou arquivo ali ou rodou git ali. Aqui vale a regra: não faça checkout, switch, rebase ou stash, e combine antes de editar.
|
|
45
|
+
- **"leu/rodou comando em"** — ela só **alcançou** o clone: leu um arquivo, entrou nele num comando. É informação, não conflito. Não pare o seu trabalho por isso e não peça para ela parar o dela.
|
|
46
|
+
|
|
47
|
+
Isso existe porque confundir os dois custou caro uma vez: um `cd` com um `sed` de leitura marcou uma sessão como estando no clone, e a outra interrompeu uma suíte em execução e pediu desculpas por um dano que não existia.
|
|
48
|
+
|
|
40
49
|
## Falar com outra sessão
|
|
41
50
|
|
|
42
51
|
O bloco diz quem está vivo; o canal serve para dizer algo a quem está vivo. Vale entre CLIs diferentes — uma sessão do Codex alcança uma do Claude, porque o transporte é o hook, que todas têm.
|