@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.
@@ -1,3 +1,3 @@
1
1
  """Portable, local-first installer for the crow-skills catalogue."""
2
2
 
3
- __version__ = "0.18.2"
3
+ __version__ = "0.19.0"
@@ -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
- where = ("MESMO clone " if root in (rel.get("sharedClones") or []) else "clone ") + short_path(root)
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
- shared = [r for r in (rel.get("sharedRepos") or []) if r != (s.get("repo") or {}).get("id")]
1588
- if shared:
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 shared[:3]))[:LINE_CHARS])
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("sharedClones") or []) for s in others):
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(repo_scope(s, now)[1]) if r in tree_roots), None)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/crow-skills",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "description": "Versioned, complementary agent skills with a protected multi-agent installer",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/release.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "crow_skills/__init__.py": {
19
19
  "executable": false,
20
20
  "kind": "file",
21
- "sha256": "985f4a517618b99f51bf9a9d98121075293b712b2a0a91b5666375d5853546ea"
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": "354f013d999adec87ccfd69e8ae2fce749593b9c2043b41116d069e57e213ccc"
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": "07b5468a007188905565bf5636d3d37919b93aae840b54a879a90625ba668d2a"
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": "5ec341809cf357d303df81a69822e7cff5fe979f2187ee623961ff197fe6cd2c"
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": "92f37cb4b5af7c738adbe218b3911a41f5cab202",
724
+ "revision": "9c7d5fe418c5c993eef7df75790263b3c77d400d",
725
725
  "schema_version": 1,
726
- "version": "0.18.2"
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.