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.
- {collab_runtime-0.6.0/collab_runtime.egg-info → collab_runtime-0.6.2}/PKG-INFO +2 -2
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/live_locks_watcher.py +3 -3
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/lock_client.py +76 -18
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/main.py +15 -2
- {collab_runtime-0.6.0 → collab_runtime-0.6.2/collab_runtime.egg-info}/PKG-INFO +2 -2
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/docs/pypi/README.md +1 -1
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/pyproject.toml +1 -1
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/LICENSE +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/README.md +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/__init__.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/__main__.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/agent_hooks.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/agent_identity.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/dashboard/dashboard-format.js +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/dashboard/index.html +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/dashboard_server.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/errors.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/githooks.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/commit-msg +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/post-commit +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/pre-commit +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/hook_templates/pre-push +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/logging_config.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/platform_probe.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/safe_subprocess.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab/subprocess_bridge.py +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/SOURCES.txt +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/dependency_links.txt +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/entry_points.txt +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/requires.txt +0 -0
- {collab_runtime-0.6.0 → collab_runtime-0.6.2}/collab_runtime.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1281
|
+
|
|
1241
1282
|
count = 0
|
|
1242
|
-
for lk in
|
|
1243
|
-
|
|
1244
|
-
if
|
|
1245
|
-
|
|
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"
|
|
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("
|
|
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
|
-
|
|
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
|
|
3269
|
-
#
|
|
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(
|
|
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.
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|