collab-runtime 0.9.0__tar.gz → 0.9.2__tar.gz

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.
Files changed (38) hide show
  1. {collab_runtime-0.9.0/collab_runtime.egg-info → collab_runtime-0.9.2}/PKG-INFO +3 -2
  2. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard_server.py +2 -1
  3. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/live_locks_watcher.py +4 -68
  4. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/lock_client.py +13 -39
  5. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/overlap.py +23 -0
  6. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/safe_subprocess.py +1 -1
  7. {collab_runtime-0.9.0 → collab_runtime-0.9.2/collab_runtime.egg-info}/PKG-INFO +3 -2
  8. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/requires.txt +1 -0
  9. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/pyproject.toml +4 -3
  10. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/LICENSE +0 -0
  11. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/README.md +0 -0
  12. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/__init__.py +0 -0
  13. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/__main__.py +0 -0
  14. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/agent_hooks.py +0 -0
  15. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/agent_identity.py +0 -0
  16. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/dashboard-charts.js +0 -0
  17. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/dashboard-filters.js +0 -0
  18. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/dashboard-format.js +0 -0
  19. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/dashboard/index.html +0 -0
  20. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/errors.py +0 -0
  21. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/githooks.py +0 -0
  22. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/commit-msg +0 -0
  23. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/post-checkout +0 -0
  24. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/post-commit +0 -0
  25. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/post-merge +0 -0
  26. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/pre-commit +0 -0
  27. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/hook_templates/pre-push +0 -0
  28. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/logging_config.py +0 -0
  29. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/main.py +0 -0
  30. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/platform_probe.py +0 -0
  31. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/pr_overlap.py +0 -0
  32. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab/subprocess_bridge.py +0 -0
  33. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/SOURCES.txt +0 -0
  34. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/dependency_links.txt +0 -0
  35. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/entry_points.txt +0 -0
  36. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/collab_runtime.egg-info/top_level.txt +0 -0
  37. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/docs/pypi/README.md +0 -0
  38. {collab_runtime-0.9.0 → collab_runtime-0.9.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Topic :: Software Development :: Version Control
16
- Requires-Python: >=3.10
16
+ Requires-Python: <3.14,>=3.10
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: python-dotenv>=1.2.2
@@ -21,6 +21,7 @@ Requires-Dist: supabase>=2.30.0
21
21
  Requires-Dist: httpx>=0.28.1
22
22
  Requires-Dist: psutil>=5.9.8
23
23
  Requires-Dist: plyer>=2.1.0
24
+ Requires-Dist: tomli>=2.0.0
24
25
  Dynamic: license-file
25
26
 
26
27
  # collab-runtime — Collaborative File Locking
@@ -17,12 +17,13 @@ import re
17
17
  import tempfile
18
18
  import threading
19
19
  import time
20
- import tomllib
21
20
  import urllib.parse
22
21
  import zipfile
23
22
  from pathlib import Path
24
23
  from typing import Any, Callable, Iterable, Optional, Sequence, Tuple
25
24
 
25
+ import tomli as tomllib
26
+
26
27
  logger = logging.getLogger(__name__)
27
28
 
28
29
  DASHBOARD_TEMP_PREFIX = ".collab-dashboard-"
@@ -781,26 +781,8 @@ def _process_releases(client, released: set[str]) -> None:
781
781
 
782
782
  Extracted so tests can simulate exceptions when removing locks from the local-owned
783
783
  set without running the entire main loop.
784
-
785
- Enforces a minimum hold time to avoid rapid acquire/release cycles when git status
786
- is transient.
787
784
  """
788
785
  for fp in released:
789
- # Enforce minimum lock hold time: skip files whose lock was acquired
790
- # too recently to avoid thrashing when git status fluctuates.
791
- acquired = _lock_acquired_at.get(fp)
792
- if acquired is not None:
793
- age = (datetime.now() - acquired).total_seconds()
794
- if age < _min_auto_lock_hold_seconds():
795
- logger.debug(
796
- "⏳ [KEPT] %s — lock is only %ds old "
797
- "(< %ds minimum); deferring auto-release",
798
- fp,
799
- int(age),
800
- _min_auto_lock_hold_seconds(),
801
- )
802
- continue
803
-
804
786
  # Was this file in conflict?
805
787
  if fp in _active_conflicts:
806
788
  _active_conflicts.discard(fp)
@@ -1205,25 +1187,10 @@ def _reconcile_on_startup(client) -> None:
1205
1187
  msg = f"[RESUMED] {fp} - lock re-adopted from this machine"
1206
1188
  logger.info(_color(msg, Fore.GREEN) if _HAS_COLORAMA else msg)
1207
1189
  else:
1208
- # File is clean — but skip if the lock was acquired too recently.
1209
- # This prevents the watcher from releasing locks it just acquired
1210
- # during the same reconciliation pass (e.g. when git status is
1211
- # transient).
1212
- acquired = _lock_acquired_at.get(fp)
1213
- if acquired is not None:
1214
- age = (datetime.now() - acquired).total_seconds()
1215
- if age < _min_auto_lock_hold_seconds():
1216
- logger.debug(
1217
- "⏳ [KEPT] %s — lock is only %ds old "
1218
- "(< %ds minimum); skipping auto-release",
1219
- fp,
1220
- int(age),
1221
- _min_auto_lock_hold_seconds(),
1222
- )
1223
- # Keep the lock in _local_owned_locks so the main loop
1224
- # continues to track it for eventual release.
1225
- _local_owned_locks.add(fp)
1226
- continue
1190
+ # File is clean — release immediately.
1191
+ # Unlike agent locks, the developer's own watcher has authoritative
1192
+ # knowledge of the working tree. Delaying here would leave
1193
+ # post-merge / post-pull files locked (#150, #151).
1227
1194
 
1228
1195
  # File is clean - stale lock, release it
1229
1196
  try:
@@ -1522,22 +1489,6 @@ def _graceful_shutdown() -> None:
1522
1489
  msg = f"[KEPT] {fp} - still has local edits, lock preserved"
1523
1490
  logger.debug(_color(msg, Fore.GREEN) if _HAS_COLORAMA else msg)
1524
1491
  else:
1525
- # Enforce minimum hold time even during shutdown:
1526
- # keep locks that were acquired too recently.
1527
- acquired = _lock_acquired_at.get(fp)
1528
- if acquired is not None:
1529
- age = (datetime.now() - acquired).total_seconds()
1530
- if age < _min_auto_lock_hold_seconds():
1531
- n_kept += 1
1532
- logger.debug(
1533
- "⏳ [KEPT] %s — lock is only %ds old "
1534
- "(< %ds minimum); preserving",
1535
- fp,
1536
- int(age),
1537
- _min_auto_lock_hold_seconds(),
1538
- )
1539
- continue
1540
-
1541
1492
  try:
1542
1493
  _scope_agent(
1543
1494
  client.table("file_locks")
@@ -1571,21 +1522,6 @@ def _graceful_shutdown() -> None:
1571
1522
  ]
1572
1523
  for fp in db_locks:
1573
1524
  if fp and fp not in still_dirty:
1574
- # Enforce minimum hold time
1575
- acquired = _lock_acquired_at.get(fp)
1576
- if acquired is not None:
1577
- age = (datetime.now() - acquired).total_seconds()
1578
- if age < _min_auto_lock_hold_seconds():
1579
- n_kept += 1
1580
- logger.debug(
1581
- "⏳ [KEPT] %s — lock is only %ds old "
1582
- "(< %ds minimum); preserving",
1583
- fp,
1584
- int(age),
1585
- _min_auto_lock_hold_seconds(),
1586
- )
1587
- continue
1588
-
1589
1525
  _scope_agent(
1590
1526
  client.table("file_locks")
1591
1527
  .delete()
@@ -3633,46 +3633,20 @@ class LockClient:
3633
3633
  if any([n_released, n_newly_locked, n_readopted, n_refreshed, n_multi]):
3634
3634
  logger.debug("Starting lock reconciliation...")
3635
3635
 
3636
- # Process stale locks — but skip any that were acquired too recently.
3637
- # This prevents the watcher from releasing locks it just acquired during
3638
- # the same reconciliation pass (e.g. when git status is transient).
3636
+ # Process stale locks — release immediately because git status
3637
+ # confirms the file is clean. Unlike agent locks (dev_other_stale),
3638
+ # the developer's own watcher has authoritative knowledge of the
3639
+ # working tree. Delaying here (e.g. via a minimum hold time) would
3640
+ # leave post-merge / post-pull files locked for minutes (#150, #151).
3639
3641
  if stale:
3640
- truly_stale = set()
3641
- for fp in stale:
3642
- lock = lock_map.get(fp, {})
3643
- acquired_str = lock.get("acquired_at")
3644
- if acquired_str:
3645
- try:
3646
- acq_dt = (
3647
- datetime.fromisoformat(acquired_str)
3648
- .astimezone()
3649
- .replace(tzinfo=None)
3650
- )
3651
- age = (_safe_now() - acq_dt).total_seconds()
3652
- if age < _min_auto_lock_hold_seconds():
3653
- kept_young.add(fp)
3654
- logger.debug(
3655
- "⏳ [KEPT] %s — lock is only %ds old "
3656
- "(< %ds minimum); skipping auto-release",
3657
- fp,
3658
- int(age),
3659
- _min_auto_lock_hold_seconds(),
3660
- )
3661
- continue
3662
- except (ValueError, TypeError):
3663
- pass
3664
- truly_stale.add(fp)
3665
-
3666
- if truly_stale:
3667
- for fp in sorted(truly_stale):
3668
- logger.info(
3669
- "🔓 [STALE-RELEASED] %s — locked but file is now clean,"
3670
- " releasing",
3671
- fp,
3672
- )
3673
- self.release_multiple(list(truly_stale))
3674
- # Only count actually-released stale locks in the summary
3675
- n_released = len(truly_stale)
3642
+ for fp in sorted(stale):
3643
+ logger.info(
3644
+ "🔓 [STALE-RELEASED] %s — locked but file is now clean,"
3645
+ " releasing",
3646
+ fp,
3647
+ )
3648
+ self.release_multiple(list(stale))
3649
+ n_released = len(stale)
3676
3650
 
3677
3651
  # Process RESUMED locks: use direct table update (preserves acquired_at)
3678
3652
  # This prevents the timer from resetting when switching IDEs
@@ -308,6 +308,11 @@ def merge_tree_conflicts(
308
308
  conflicted.add(line.strip())
309
309
  # rc==1 with nothing parseable -> inconclusive; fall back to file-level.
310
310
  return frozenset(conflicted) if conflicted else None
311
+ # rc != 0,1: the merge-tree command itself failed (e.g. rc=128 "refusing to
312
+ # merge unrelated histories" in a shallow clone, or unsupported git version).
313
+ logger.debug(
314
+ "merge-tree exited rc=%d for %s..%s; inconclusive", rc, head_ref, other_ref
315
+ )
311
316
  return None
312
317
 
313
318
 
@@ -376,6 +381,24 @@ def fetch_pr_ref(
376
381
  "Fetched SHA %s != expected %s for PR #%d", fetched_sha, head_sha, pr_number
377
382
  )
378
383
  # Still usable (the ref exists), but log the discrepancy.
384
+
385
+ # If the checkout is shallow (e.g. actions/checkout@v4 default fetch-depth: 1),
386
+ # unshallow now so that git merge-tree can find the merge base. Without this,
387
+ # merge-tree exits rc=128 ("refusing to merge unrelated histories") and
388
+ # merge_tree_conflicts returns None (inconclusive).
389
+ rc_shallow, shallow_out = capture(["rev-parse", "--is-shallow-repository"])
390
+ if rc_shallow == 0 and shallow_out.strip() == "true":
391
+ logger.debug("Shallow clone detected; unshallowing from %s", remote)
392
+ rc_unshallow, _ = capture(["fetch", "--unshallow", remote])
393
+ if rc_unshallow != 0:
394
+ # Best-effort fallback: deepen history so merge-tree has a
395
+ # reasonable chance of finding the merge base.
396
+ logger.debug(
397
+ "--unshallow failed (rc=%d); trying --deepen=50 as fallback",
398
+ rc_unshallow,
399
+ )
400
+ capture(["fetch", "--deepen=50", remote])
401
+
379
402
  return local_ref
380
403
 
381
404
 
@@ -272,7 +272,7 @@ def capture(
272
272
  sp = get_subprocess()
273
273
  safe_argv = validate_argv(argv, policy=policy)
274
274
  kwargs: dict[str, Any] = {
275
- "stderr": sp.DEVNULL,
275
+ "stderr": sp.PIPE,
276
276
  "timeout": timeout,
277
277
  "cwd": cwd,
278
278
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Topic :: Software Development :: Version Control
16
- Requires-Python: >=3.10
16
+ Requires-Python: <3.14,>=3.10
17
17
  Description-Content-Type: text/markdown
18
18
  License-File: LICENSE
19
19
  Requires-Dist: python-dotenv>=1.2.2
@@ -21,6 +21,7 @@ Requires-Dist: supabase>=2.30.0
21
21
  Requires-Dist: httpx>=0.28.1
22
22
  Requires-Dist: psutil>=5.9.8
23
23
  Requires-Dist: plyer>=2.1.0
24
+ Requires-Dist: tomli>=2.0.0
24
25
  Dynamic: license-file
25
26
 
26
27
  # collab-runtime — Collaborative File Locking
@@ -3,3 +3,4 @@ supabase>=2.30.0
3
3
  httpx>=0.28.1
4
4
  psutil>=5.9.8
5
5
  plyer>=2.1.0
6
+ tomli>=2.0.0
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "collab-runtime"
7
- version = "0.9.0"
7
+ version = "0.9.2"
8
8
  description = "Collaborative file locking runtime"
9
9
  readme = "docs/pypi/README.md"
10
10
  license = "MIT"
11
- requires-python = ">=3.10"
11
+ requires-python = ">=3.10,<3.14"
12
12
  authors = [
13
13
  { name = "KirilMT", email = "kiril.mt95@gmail.com" }
14
14
  ]
@@ -28,7 +28,8 @@ dependencies = [
28
28
  "supabase>=2.30.0",
29
29
  "httpx>=0.28.1",
30
30
  "psutil>=5.9.8",
31
- "plyer>=2.1.0"
31
+ "plyer>=2.1.0",
32
+ "tomli>=2.0.0",
32
33
  ]
33
34
 
34
35
  [project.scripts]
File without changes
File without changes
File without changes