collab-runtime 0.6.0__tar.gz → 0.6.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 (32) hide show
  1. {collab_runtime-0.6.0/collab_runtime.egg-info → collab_runtime-0.6.2}/PKG-INFO +2 -2
  2. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/live_locks_watcher.py +3 -3
  3. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/lock_client.py +76 -18
  4. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/main.py +15 -2
  5. {collab_runtime-0.6.0 → collab_runtime-0.6.2/collab_runtime.egg-info}/PKG-INFO +2 -2
  6. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/docs/pypi/README.md +1 -1
  7. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/pyproject.toml +1 -1
  8. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/LICENSE +0 -0
  9. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/README.md +0 -0
  10. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/__init__.py +0 -0
  11. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/__main__.py +0 -0
  12. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/agent_hooks.py +0 -0
  13. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/agent_identity.py +0 -0
  14. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/dashboard/dashboard-format.js +0 -0
  15. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/dashboard/index.html +0 -0
  16. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/dashboard_server.py +0 -0
  17. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/errors.py +0 -0
  18. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/githooks.py +0 -0
  19. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/commit-msg +0 -0
  20. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/post-commit +0 -0
  21. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/pre-commit +0 -0
  22. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/pre-push +0 -0
  23. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/logging_config.py +0 -0
  24. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/platform_probe.py +0 -0
  25. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/safe_subprocess.py +0 -0
  26. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/subprocess_bridge.py +0 -0
  27. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/SOURCES.txt +0 -0
  28. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/dependency_links.txt +0 -0
  29. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/entry_points.txt +0 -0
  30. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/requires.txt +0 -0
  31. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/top_level.txt +0 -0
  32. {collab_runtime-0.6.0 → collab_runtime-0.6.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -164,7 +164,7 @@ collab release path/to/file.py
164
164
  # Check lock status for a specific file
165
165
  collab status path/to/file.py
166
166
 
167
- # Release all locks held by you
167
+ # Release all locks held by you (including your own AI-agent locks)
168
168
  collab release-all
169
169
 
170
170
  # Force release (requires SUPABASE_SERVICE_ROLE_KEY)
@@ -822,9 +822,9 @@ def _fetch_dev_other_identity_locks(client) -> dict[str, dict]:
822
822
  def _filter_agent_held_new_files(client, new_files: set[str]) -> set[str]:
823
823
  """Drop files already held by this developer's AI agent from the auto-lock set.
824
824
 
825
- Prevents the human watcher from fighting (and logging false CONFLICTs for) the
826
- developer's own agent locks. The ``acquire_lock`` RPC lets an agent take over a
827
- human auto-lock, never the reverse, so the human watcher must simply step aside.
825
+ Prevents the human watcher from downgrading the developer's own agent locks during
826
+ bulk auto-watch. Explicit ``acquire`` / pre-commit paths may still re-own same-
827
+ developer agent locks via the ``acquire_lock`` RPC.
828
828
  """
829
829
  if not new_files:
830
830
  return new_files
@@ -1118,6 +1118,11 @@ class LockClient:
1118
1118
  def release(self, file_path: str) -> Tuple[bool, str]:
1119
1119
  """Release a lock on file_path owned by this developer.
1120
1120
 
1121
+ When called by a **human** (``agent_id`` is ``None``), any lock owned by this
1122
+ ``developer_id`` is released regardless of which agent claimed it — the human is
1123
+ in charge of all their agents. When called by an **agent** (``agent_id`` is
1124
+ set), only locks claimed by that specific agent identity are released.
1125
+
1121
1126
  Returns (success: bool, message: str).
1122
1127
  """
1123
1128
  # If ephemeral, nothing was persisted so there's nothing to delete.
@@ -1127,16 +1132,45 @@ class LockClient:
1127
1132
  )
1128
1133
  return True, "ephemeral-released"
1129
1134
 
1135
+ norm = self._normalize_file_path(file_path)
1130
1136
  client = self._require_client()
1137
+
1138
+ # Pre-check: verify a lock row exists for this file *and* belongs to
1139
+ # this developer before attempting the DELETE. PostgREST returns
1140
+ # 204 No Content even when zero rows are deleted, so without this
1141
+ # guard the CLI would falsely report "✓ released" for locks that
1142
+ # belong to another developer or do not exist at all.
1143
+ try:
1144
+ check_res = _retry_on_network_error(
1145
+ lambda: client.table("file_locks")
1146
+ .select("developer_id")
1147
+ .eq("file_path", norm)
1148
+ .execute()
1149
+ )
1150
+ except Exception as e:
1151
+ return False, f"API Error: {e}"
1152
+ _st, rows, _err = self._parse_response(check_res)
1153
+ if not rows or not isinstance(rows, list) or len(rows) == 0:
1154
+ return False, f"No lock found for: {file_path}"
1155
+ lock_owner = rows[0].get("developer_id")
1156
+ if lock_owner != self.developer_id:
1157
+ return False, (
1158
+ f"Permission denied: {file_path} is locked by @{lock_owner or '?'}. "
1159
+ "Use `collab force-release` if you have admin credentials."
1160
+ )
1161
+
1131
1162
  try:
1132
- norm = self._normalize_file_path(file_path)
1133
1163
  delete_query = (
1134
1164
  client.table("file_locks")
1135
1165
  .delete()
1136
1166
  .eq("file_path", norm)
1137
1167
  .eq("developer_id", self.developer_id)
1138
1168
  )
1139
- delete_query = self._apply_agent_scope(delete_query)
1169
+ # Human (agent_id is None): developer-scoped — release any agent's
1170
+ # lock. Agent (agent_id is set): identity-scoped — only release
1171
+ # this specific agent's lock.
1172
+ if self.agent_id is not None:
1173
+ delete_query = delete_query.eq("agent_id", self.agent_id)
1140
1174
  res = _retry_on_network_error(lambda: delete_query.execute())
1141
1175
  except Exception as e:
1142
1176
  return False, f"API Error: {e}"
@@ -1145,9 +1179,7 @@ class LockClient:
1145
1179
  if error:
1146
1180
  return False, f"API Error: {error}"
1147
1181
  if status in (200, 204) or data is not None:
1148
- logger.info(
1149
- "🔓 [RELEASED] %s — lock released", self._normalize_file_path(file_path)
1150
- )
1182
+ logger.info("🔓 [RELEASED] %s — lock released", norm)
1151
1183
  return True, "released"
1152
1184
  return False, "No lock released (not owner or lock does not exist)"
1153
1185
 
@@ -1227,22 +1259,45 @@ class LockClient:
1227
1259
  "can_edit": self._lock_owned_by_me(lock),
1228
1260
  }
1229
1261
 
1230
- def release_all(self) -> int:
1231
- """Release all locks held by this developer.
1262
+ def release_all(self, include_agent: bool = True) -> int:
1263
+ """Release locks held by this developer.
1264
+
1265
+ By default (``include_agent=True``) every lock owned by this ``developer_id`` is
1266
+ released regardless of ``agent_id`` — both the human auto-locks and this
1267
+ developer's own AI-agent locks. This lets a human session fully clear locks left
1268
+ behind by its own agents (for example stale agent locks after a session ended
1269
+ without pushing). Genuine cross-developer locks are never touched.
1270
+
1271
+ Set ``include_agent=False`` to restrict cleanup to the current ``(developer_id,
1272
+ agent_id)`` identity only.
1232
1273
 
1233
- Returns count released.
1274
+ Returns the number of locks released.
1234
1275
  """
1235
1276
  try:
1236
1277
  locks = self.active()
1237
1278
  except LockServiceUnavailableError as exc:
1238
1279
  logger.error("release_all skipped — lock service unavailable: %s", exc)
1239
1280
  return 0
1240
- my_locks = [lk for lk in locks if self._lock_owned_by_me(lk)]
1281
+
1241
1282
  count = 0
1242
- for lk in my_locks:
1243
- ok, _ = self.release(lk.get("file_path", ""))
1244
- if ok:
1245
- count += 1
1283
+ for lk in locks:
1284
+ file_path = lk.get("file_path", "")
1285
+ if not file_path:
1286
+ continue
1287
+ if include_agent:
1288
+ # Developer-scoped: clear any lock under our developer_id,
1289
+ # including this developer's own agent identities.
1290
+ if lk.get("developer_id") != self.developer_id:
1291
+ continue
1292
+ if self._release_developer_scope(file_path):
1293
+ count += 1
1294
+ else:
1295
+ # Identity-scoped: only the current (developer_id, agent_id).
1296
+ if not self._lock_owned_by_me(lk):
1297
+ continue
1298
+ ok, _ = self.release(file_path)
1299
+ if ok:
1300
+ count += 1
1246
1301
  return count
1247
1302
 
1248
1303
  def force_release(self, file_path: str) -> Tuple[bool, str]:
@@ -1985,7 +2040,7 @@ class LockClient:
1985
2040
  try:
1986
2041
  pid = self._read_pid(strict=True)
1987
2042
  except PidParseError as exc:
1988
- print(f"Lock watcher status unavailable: {exc.message}")
2043
+ print(f"ℹ️ Lock watcher status unavailable: {exc.message}")
1989
2044
  return False
1990
2045
  local_only_mode = bool(getattr(self, "local_only", False))
1991
2046
  if pid and self._is_process_alive(pid):
@@ -2114,7 +2169,7 @@ class LockClient:
2114
2169
  return True
2115
2170
  except (ValueError, OSError):
2116
2171
  pass
2117
- print("Lock watcher is NOT running.")
2172
+ print("ℹ️ Lock watcher is not running.")
2118
2173
  return False
2119
2174
 
2120
2175
  def cleanup_orphaned_processes(self) -> None:
@@ -3117,7 +3172,10 @@ class LockClient:
3117
3172
  try:
3118
3173
  shutdown_file = _state_path(".shutdown_complete")
3119
3174
  with open(shutdown_file, "w") as f:
3120
- f.write(f"{n_kept}\n")
3175
+ # Write -1 sentinel when enumeration failed so that IDE
3176
+ # extensions can distinguish "0 locks held" (normal) from
3177
+ # "could not verify" (transient service error).
3178
+ f.write(f"{-1 if not enumeration_ok else n_kept}\n")
3121
3179
  f.flush()
3122
3180
  try:
3123
3181
  os.fsync(f.fileno())
@@ -3265,8 +3323,8 @@ class LockClient:
3265
3323
 
3266
3324
  # Calculate lock categories. ``missing`` excludes files already held by
3267
3325
  # this developer under another (agent) identity so the human watcher does
3268
- # not generate conflicts trying to re-lock an agent's file. Agent claims
3269
- # take over human auto-locks atomically in the acquire_lock RPC instead.
3326
+ # not downgrade agent attribution during bulk reconcile. Explicit acquire
3327
+ # (e.g. pre-commit) may still re-own same-developer agent locks via RPC.
3270
3328
  stale = my_locks - git_modified
3271
3329
  missing = git_modified - my_locks - dev_other_locked
3272
3330
  still_valid = my_locks & git_modified
@@ -152,7 +152,18 @@ def _run_cli() -> None:
152
152
  st.add_argument("file_path")
153
153
 
154
154
  # release-all
155
- sub.add_parser("release-all", help="Release all locks held by you")
155
+ ra = sub.add_parser(
156
+ "release-all",
157
+ help="Release all locks held by you (including your AI-agent locks)",
158
+ )
159
+ ra.add_argument(
160
+ "--identity-only",
161
+ action="store_true",
162
+ help=(
163
+ "Only release locks for the current identity (developer + agent), "
164
+ "not every lock owned by your developer id"
165
+ ),
166
+ )
156
167
 
157
168
  # force-release
158
169
  fr = sub.add_parser(
@@ -473,7 +484,9 @@ def _run_cli() -> None:
473
484
  print("🔓 File is unlocked.")
474
485
 
475
486
  elif args.command == "release-all":
476
- count = client.release_all()
487
+ count = client.release_all(
488
+ include_agent=not getattr(args, "identity_only", False)
489
+ )
477
490
  print(f"Released {count} lock(s).")
478
491
 
479
492
  elif args.command == "force-release":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -164,7 +164,7 @@ collab release path/to/file.py
164
164
  # Check lock status for a specific file
165
165
  collab status path/to/file.py
166
166
 
167
- # Release all locks held by you
167
+ # Release all locks held by you (including your own AI-agent locks)
168
168
  collab release-all
169
169
 
170
170
  # Force release (requires SUPABASE_SERVICE_ROLE_KEY)
@@ -139,7 +139,7 @@ collab release path/to/file.py
139
139
  # Check lock status for a specific file
140
140
  collab status path/to/file.py
141
141
 
142
- # Release all locks held by you
142
+ # Release all locks held by you (including your own AI-agent locks)
143
143
  collab release-all
144
144
 
145
145
  # Force release (requires SUPABASE_SERVICE_ROLE_KEY)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "collab-runtime"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "Collaborative file locking runtime"
9
9
  readme = "docs/pypi/README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes