@andespindola/crow-skills 0.18.2 → 0.19.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
CHANGED
package/crow_skills/broadcast.py
CHANGED
|
@@ -905,6 +905,49 @@ def repo_scope(snap, now=None, window=None):
|
|
|
905
905
|
return ids, roots
|
|
906
906
|
|
|
907
907
|
|
|
908
|
+
|
|
909
|
+
def occupancy(snap, now=None, window=None):
|
|
910
|
+
"""The clones a session OCCUPIES, as opposed to the ones it merely reached.
|
|
911
|
+
|
|
912
|
+
Occupied: the cwd clone, a clone where it edited a file, and a clone a git command actually
|
|
913
|
+
drove (``driven``). Reached-only is a clone a shell call read or cd'ed into — and conflating the
|
|
914
|
+
two has a measured cost: a read-only ``cd`` plus a ``sed`` of one doc marked a session as using
|
|
915
|
+
a clone, which made a peer session stop a running test suite and apologise for damage to work
|
|
916
|
+
that did not exist there. Reach stays visible as information; only occupancy warns.
|
|
917
|
+
"""
|
|
918
|
+
now = now or time.time()
|
|
919
|
+
window = FILE_TTL if window is None else window
|
|
920
|
+
ids, roots = set(), set()
|
|
921
|
+
presence = presence_of(snap)
|
|
922
|
+
repo = snap.get("repo") if isinstance(snap.get("repo"), dict) else None
|
|
923
|
+
if repo is None:
|
|
924
|
+
repo = presence.get("repo") if isinstance(presence.get("repo"), dict) else None
|
|
925
|
+
if repo is None and snap.get("cwd"):
|
|
926
|
+
repo = repo_info(snap["cwd"])
|
|
927
|
+
if isinstance(repo, dict): # where the session lives
|
|
928
|
+
if repo.get("id"):
|
|
929
|
+
ids.add(repo["id"])
|
|
930
|
+
if repo.get("root"):
|
|
931
|
+
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("driven"):
|
|
934
|
+
continue
|
|
935
|
+
if now - (parse_time(entry.get("ts")) or 0) > window:
|
|
936
|
+
continue
|
|
937
|
+
if isinstance(entry.get("id"), str):
|
|
938
|
+
ids.add(entry["id"])
|
|
939
|
+
if isinstance(entry.get("root"), str):
|
|
940
|
+
roots.add(entry["root"])
|
|
941
|
+
for item in recent_files(snap, now, window): # and the clones whose files it edited
|
|
942
|
+
if isinstance(item.get("repo"), str):
|
|
943
|
+
ids.add(item["repo"])
|
|
944
|
+
rel, path = item.get("rel"), item.get("path")
|
|
945
|
+
if isinstance(rel, str) and rel and isinstance(path, str) and path.endswith(rel):
|
|
946
|
+
root = path[: len(path) - len(rel)].rstrip("/")
|
|
947
|
+
if root:
|
|
948
|
+
roots.add(root)
|
|
949
|
+
return ids, roots
|
|
950
|
+
|
|
908
951
|
def same_file(a, b):
|
|
909
952
|
"""Two touched-file entries name the same file: same path, or same repo id and repo-relative path."""
|
|
910
953
|
if a.get("path") == b.get("path"):
|
|
@@ -1492,8 +1535,14 @@ def relations(me, other, now, cfg):
|
|
|
1492
1535
|
my_ids, my_roots = repo_scope(me, now)
|
|
1493
1536
|
their_ids, their_roots = repo_scope(other, now)
|
|
1494
1537
|
shared_roots = sorted(my_roots & their_roots)
|
|
1538
|
+
# Reach and occupancy are different claims about the same clone: `sharedClones` is "both sides
|
|
1539
|
+
# touched it", `occupiedClones` is "both sides are WORKING in it". Only the second warns.
|
|
1540
|
+
my_live, their_live = occupancy(me, now), occupancy(other, now)
|
|
1541
|
+
occupied_roots = sorted(my_live[1] & their_live[1])
|
|
1542
|
+
occupied_ids = sorted(my_live[0] & their_live[0])
|
|
1495
1543
|
rel = {"sameRepo": bool(my_ids & their_ids), "sameClone": bool(shared_roots),
|
|
1496
1544
|
"sharedRepos": sorted(my_ids & their_ids), "sharedClones": shared_roots,
|
|
1545
|
+
"occupiedClones": occupied_roots, "occupiedRepos": occupied_ids,
|
|
1497
1546
|
"sameBranch": False, "overlap": []}
|
|
1498
1547
|
rel["sameBranch"] = bool(mine.get("id") and mine.get("id") == theirs.get("id") and me.get("branch")
|
|
1499
1548
|
and me.get("branch") == other.get("branch"))
|
|
@@ -1569,7 +1618,11 @@ def session_lines(s, home, now, with_files=True):
|
|
|
1569
1618
|
state = "bloqueada" if s.get("blocked") else {"working": "trabalhando", "idle": "ociosa", "blocked": "bloqueada",
|
|
1570
1619
|
"error": "com erro"}.get(s.get("state"), clean(s.get("state") or "?", 12))
|
|
1571
1620
|
root = (s.get("repo") or {}).get("root") or s.get("cwd") or "?"
|
|
1572
|
-
|
|
1621
|
+
occupied = rel.get("occupiedClones")
|
|
1622
|
+
occupied = occupied if isinstance(occupied, list) else (rel.get("sharedClones") or [])
|
|
1623
|
+
where = ("MESMO clone " if root in occupied
|
|
1624
|
+
else "leu/rodou comando em " if root in (rel.get("sharedClones") or [])
|
|
1625
|
+
else "clone ") + short_path(root)
|
|
1573
1626
|
head = "- " + clean(s.get("host"), 12) + " " + state + " · branch " + clean(s.get("branch") or "?", 60) + \
|
|
1574
1627
|
(" (a mesma)" if rel.get("sameBranch") else "") + " · " + where + " · " + age_text(s.get("ageSeconds"))
|
|
1575
1628
|
lines = [head[:LINE_CHARS]]
|
|
@@ -1584,10 +1637,18 @@ def session_lines(s, home, now, with_files=True):
|
|
|
1584
1637
|
names = [clean(f["rel"], 80) if f.get("rel") else short_path(f["path"], 60) for f in reversed(files)]
|
|
1585
1638
|
shown = ", ".join(names[:4]) + (" (+" + str(len(names) - 4) + ")" if len(names) > 4 else "")
|
|
1586
1639
|
lines.append((" arquivos editados: " + shown)[:LINE_CHARS])
|
|
1587
|
-
|
|
1588
|
-
if
|
|
1640
|
+
mine_id = (s.get("repo") or {}).get("id")
|
|
1641
|
+
shared = [r for r in (rel.get("sharedRepos") or []) if r != mine_id]
|
|
1642
|
+
# "trabalha em" was claiming too much: a read-only visit landed here too, and a peer session
|
|
1643
|
+
# stopped a running suite over it. The two claims are now separate lines.
|
|
1644
|
+
busy = [r for r in shared if r in (rel.get("occupiedRepos") or [])]
|
|
1645
|
+
read = [r for r in shared if r not in (rel.get("occupiedRepos") or [])]
|
|
1646
|
+
if busy:
|
|
1589
1647
|
lines.append((" TAMBÉM trabalha em repositório(s) deste trabalho: " +
|
|
1590
|
-
", ".join(clean(r, 60) for r in
|
|
1648
|
+
", ".join(clean(r, 60) for r in busy[:3]))[:LINE_CHARS])
|
|
1649
|
+
if read:
|
|
1650
|
+
lines.append((" leu/rodou comando em repositório(s) deste trabalho (sem editar): " +
|
|
1651
|
+
", ".join(clean(r, 60) for r in read[:3]))[:LINE_CHARS])
|
|
1591
1652
|
if rel.get("overlap"):
|
|
1592
1653
|
lines.append((" SOBREPOSIÇÃO com arquivos desta sessão: " + ", ".join(clean(x, 80) for x in rel["overlap"][:4]))[:LINE_CHARS])
|
|
1593
1654
|
return lines
|
|
@@ -1660,7 +1721,7 @@ def digest(home, me_key, me=None, now=None, cfg=None, deliver=True):
|
|
|
1660
1721
|
lines.append(claim_line(claim, live_by_key, now))
|
|
1661
1722
|
my_root = (view["me"].get("repo") or {}).get("root")
|
|
1662
1723
|
tail = []
|
|
1663
|
-
if my_root and any(my_root in ((s.get("relation") or {}).get("
|
|
1724
|
+
if my_root and any(my_root in ((s.get("relation") or {}).get("occupiedClones") or []) for s in others):
|
|
1664
1725
|
tail.append("ATENÇÃO: outra sessão viva trabalha neste mesmo clone (cwd, edição ou comando); "
|
|
1665
1726
|
"não faça checkout/switch/rebase/stash aqui.")
|
|
1666
1727
|
tail.append(FOOTER)
|
|
@@ -1710,7 +1771,7 @@ def check(home, me_key, me, paths=(), tree_change=(), now=None, cfg=None):
|
|
|
1710
1771
|
warnings.append("- " + clean(", ".join(hits[:4]), 160) + ": editado " + age_text(s.get("ageSeconds")) + " por " +
|
|
1711
1772
|
clean(s.get("host"), 12) + " (branch " + clean(s.get("branch") or "?", 60) + ", " +
|
|
1712
1773
|
short_path((s.get("repo") or {}).get("root") or s.get("cwd") or "?") + ")")
|
|
1713
|
-
theirs_root = next((r for r in sorted(
|
|
1774
|
+
theirs_root = next((r for r in sorted(occupancy(s, now)[1]) if r in tree_roots), None)
|
|
1714
1775
|
if theirs_root:
|
|
1715
1776
|
warnings.append("- o clone " + short_path(theirs_root) + " está em uso por " + clean(s.get("host"), 12) +
|
|
1716
1777
|
" 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": "7720abacfffca6315ecfee7b1dfced20c3c6e7387f00113ce2c437ed63f8e0bc"
|
|
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": "998cdd9bc67d52e63dfcde5528c5caaa04c509ec6e1e7d6afb298c8bcf4a442f"
|
|
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": "b70074096ec8ed3994a6cff3eeb2b72885ec366d01269f9006d0e492fccf649a"
|
|
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": "9c7d5fe418c5c993eef7df75790263b3c77d400d",
|
|
725
725
|
"schema_version": 1,
|
|
726
|
-
"version": "0.
|
|
726
|
+
"version": "0.19.0"
|
|
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.
|