collab-runtime 0.6.2__tar.gz → 0.8.0__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 (39) hide show
  1. {collab_runtime-0.6.2/collab_runtime.egg-info → collab_runtime-0.8.0}/PKG-INFO +17 -2
  2. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/README.md +29 -15
  3. collab_runtime-0.8.0/collab/dashboard/dashboard-charts.js +248 -0
  4. collab_runtime-0.8.0/collab/dashboard/dashboard-filters.js +427 -0
  5. collab_runtime-0.8.0/collab/dashboard/index.html +3409 -0
  6. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/dashboard_server.py +192 -7
  7. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/githooks.py +75 -6
  8. collab_runtime-0.8.0/collab/hook_templates/post-checkout +38 -0
  9. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/hook_templates/post-commit +1 -1
  10. collab_runtime-0.8.0/collab/hook_templates/post-merge +42 -0
  11. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/hook_templates/pre-push +14 -0
  12. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/live_locks_watcher.py +129 -84
  13. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/lock_client.py +248 -95
  14. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/main.py +197 -0
  15. collab_runtime-0.8.0/collab/overlap.py +595 -0
  16. collab_runtime-0.8.0/collab/pr_overlap.py +267 -0
  17. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/safe_subprocess.py +6 -0
  18. {collab_runtime-0.6.2 → collab_runtime-0.8.0/collab_runtime.egg-info}/PKG-INFO +17 -2
  19. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab_runtime.egg-info/SOURCES.txt +6 -0
  20. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/docs/pypi/README.md +16 -1
  21. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/pyproject.toml +16 -1
  22. collab_runtime-0.6.2/collab/dashboard/index.html +0 -1367
  23. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/LICENSE +0 -0
  24. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/__init__.py +0 -0
  25. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/__main__.py +0 -0
  26. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/agent_hooks.py +0 -0
  27. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/agent_identity.py +0 -0
  28. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/dashboard/dashboard-format.js +0 -0
  29. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/errors.py +0 -0
  30. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/hook_templates/commit-msg +0 -0
  31. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/hook_templates/pre-commit +0 -0
  32. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/logging_config.py +0 -0
  33. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/platform_probe.py +0 -0
  34. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab/subprocess_bridge.py +0 -0
  35. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab_runtime.egg-info/dependency_links.txt +0 -0
  36. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab_runtime.egg-info/entry_points.txt +0 -0
  37. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab_runtime.egg-info/requires.txt +0 -0
  38. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/collab_runtime.egg-info/top_level.txt +0 -0
  39. {collab_runtime-0.6.2 → collab_runtime-0.8.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.6.2
3
+ Version: 0.8.0
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -94,6 +94,11 @@ LOCK_STRICT=0 # 1 = block on lock errors, 0
94
94
  COLLAB_AGENT_ID=agent-my-task # Optional: unique id per AI agent session
95
95
  COLLAB_AGENT_LABEL=refactor-auth # Optional display label
96
96
  COLLAB_AGENT_MODE=1 # Auto-generate/persist agent id when unset
97
+ COLLAB_OVERLAP_STRICT=0 # 1 = block git push on cross-branch overlaps (fail-closed)
98
+ COLLAB_OVERLAP_FETCH=auto # auto = fetch only in strict; 1/0 to force/skip
99
+ COLLAB_OVERLAP_LINE_LEVEL=1 # 0 = file-level instead of git merge-tree (line-level)
100
+ COLLAB_OVERLAP_REMOTE= # override the auto-detected remote (e.g. upstream)
101
+ COLLAB_PR_CLAIMS=0 # 1 = keep a pushed branch's files claimed until merged
97
102
  ```
98
103
 
99
104
  > **Keep `SUPABASE_SERVICE_ROLE_KEY` private — never commit it to version control.**
@@ -138,7 +143,17 @@ collab init-hooks
138
143
 
139
144
  This installs `pre-commit`, `post-commit`, `pre-push`, and `commit-msg` hooks into the current
140
145
  repository. The hooks acquire locks for staged files, block commits that conflict with another
141
- developer's lock, and release locks after a successful push. They resolve the project `.venv` first,
146
+ developer's lock, and release locks after a successful push. By default, Collab warns about
147
+ overlaps across unmerged branches during push; set `COLLAB_OVERLAP_STRICT=1` to block the push
148
+ when overlaps are detected (fail-closed, and the hook first runs `git fetch` so branches pushed
149
+ from other clones are seen). Overlap is confirmed at line level via `git merge-tree`, so edits to
150
+ different regions of the same file are not flagged, and the comparison remote is auto-detected.
151
+ For enforcement that cannot be bypassed with `git push --no-verify`,
152
+ add the `PR Overlap Guard` GitHub Action to your branch-protection required checks.
153
+ For **edit-time** protection across open PRs, set `COLLAB_PR_CLAIMS=1`: a pushed branch's
154
+ files stay claimed (so other developers are warned/blocked as they edit) until the branch is
155
+ merged or deleted — this requires re-running `supabase/schema.sql` (idempotent migration). The hooks
156
+ resolve the project `.venv` first,
142
157
  so **commits from VS Code / Cursor Source Control behave the same as a venv-activated terminal**.
143
158
 
144
159
  Existing non-collab hooks are preserved; pass `--force` to overwrite them.
@@ -77,18 +77,24 @@ The setup script automatically:
77
77
  After setup, your `.env` at the project root is ready to use. The team Supabase URL and
78
78
  anon key come pre-configured from `.env.example`:
79
79
 
80
- | Variable | Description |
81
- | --------------------------- | ------------------------------------------------------------------- |
82
- | `SUPABASE_URL` | Your Supabase project URL (pre-configured from `.env.example`) |
83
- | `SUPABASE_ANON_KEY` | Anonymous/public key (pre-configured; safe to commit — see below) |
84
- | `SUPABASE_SERVICE_ROLE_KEY` | Service role key (**required** for dashboard force-release) |
85
- | `LOCK_STRICT` | If `1`, git hooks block on lock errors. Default `0` (warn only) |
86
- | `COLLAB_AGENT_ID` | Optional stable id for an AI agent session (multi-agent locking) |
87
- | `COLLAB_AGENT_LABEL` | Optional task label shown on the dashboard (e.g. `refactor-auth`) |
88
- | `COLLAB_AGENT_KIND` | Optional AI runtime for the dashboard icon (auto-detected) |
89
- | `COLLAB_AGENT_MODE` | Set to `1` to auto-generate/persist an agent id when unset |
90
- | `COLLAB_AGENT_HOOKS` | Set to `1` to enable the IDE edit hook that auto-claims agent edits |
91
- | `COLLAB_WATCHER_AGENT_ID` | Opt in to a dedicated agent watcher (default: watcher = human) |
80
+ | Variable | Description |
81
+ | --------------------------- | --------------------------------------------------------------------- |
82
+ | `SUPABASE_URL` | Your Supabase project URL (pre-configured from `.env.example`) |
83
+ | `SUPABASE_ANON_KEY` | Anonymous/public key (pre-configured; safe to commit — see below) |
84
+ | `SUPABASE_SERVICE_ROLE_KEY` | Service role key (**required** for dashboard force-release) |
85
+ | `LOCK_STRICT` | If `1`, git hooks block on lock errors. Default `0` (warn only) |
86
+ | `COLLAB_AGENT_ID` | Optional stable id for an AI agent session (multi-agent locking) |
87
+ | `COLLAB_AGENT_LABEL` | Optional task label shown on the dashboard (e.g. `refactor-auth`) |
88
+ | `COLLAB_AGENT_KIND` | Optional AI runtime for the dashboard icon (auto-detected) |
89
+ | `COLLAB_AGENT_MODE` | Set to `1` to auto-generate/persist an agent id when unset |
90
+ | `COLLAB_AGENT_HOOKS` | Set to `1` to enable the IDE edit hook that auto-claims agent edits |
91
+ | `COLLAB_WATCHER_AGENT_ID` | Opt in to a dedicated agent watcher (default: watcher = human) |
92
+ | `COLLAB_OVERLAP_STRICT` | If `1`, git push blocks on cross-branch overlap (implies the check) |
93
+ | `COLLAB_OVERLAP_CHECK` | If `0`, disable overlap warnings entirely (ignored when strict) |
94
+ | `COLLAB_OVERLAP_FETCH` | `auto` (default: fetch only in strict), or `1`/`0` to force/skip |
95
+ | `COLLAB_OVERLAP_LINE_LEVEL` | If `0`, use file-level overlap instead of `git merge-tree` (line) |
96
+ | `COLLAB_OVERLAP_REMOTE` | Remote to compare against (default: auto-detected, e.g. `origin`) |
97
+ | `COLLAB_PR_CLAIMS` | If `1`, keep a pushed branch's files claimed until merged (see below) |
92
98
 
93
99
  > **Important:** `SUPABASE_SERVICE_ROLE_KEY` is needed for the dashboard's Force Release button.
94
100
  > Without it, only your own locks can be released. Obtain it from a maintainer — **never commit it**.
@@ -479,9 +485,17 @@ collab daemon-status
479
485
 
480
486
  ### Conflict Prevention
481
487
 
482
- - File locks use a unique key on `file_path`
483
- - Only one developer can hold a lock per file
484
- - Merge conflicts prevented by design
488
+ - File locks use a unique key on `file_path`.
489
+ - Only one developer can hold a lock per file simultaneously.
490
+ - **Lock Lifecycle**: Locks are held during local editing and committed changes. They are automatically released after a successful `git push` via the pre-push hook.
491
+ - **Cross-Branch Overlap (client)**: By default, Collab warns if a change you are pushing would conflict with another unmerged branch. To **block** the push in this case, set `COLLAB_OVERLAP_STRICT=1`.
492
+ - **Line-level accuracy**: overlap is confirmed with a real in-memory merge (`git merge-tree`), so two branches editing **different regions** of the same file are not flagged. On git older than 2.38 it falls back to file-level. Force file-level with `COLLAB_OVERLAP_LINE_LEVEL=0`.
493
+ - **Remote-aware**: the remote to compare against is auto-detected (the push target, the branch upstream, `origin`, or the sole remote); override with `COLLAB_OVERLAP_REMOTE`.
494
+ - **Fresh state**: in strict mode the pre-push hook runs `git fetch` first so branches pushed from **other** clones are visible. This is **fail-closed** — if the fetch or check cannot complete, the push is blocked. `COLLAB_OVERLAP_FETCH` is `auto` by default (fetch only in strict, to avoid adding a fetch to every advisory push); set `1`/`0` to force or skip.
495
+ - Branches you are stacked on top of (ancestors of `HEAD`) are not flagged.
496
+ - When strict mode blocks you: rebase onto / coordinate merge order with the other branch, or override for a single push with `COLLAB_OVERLAP_STRICT=0` (last resort: `git push --no-verify`).
497
+ - **Cross-PR Overlap (server, bulletproof)**: The [`PR Overlap Guard`](.github/workflows/pr-overlap-guard.yml) workflow fails a PR check when its changed files overlap another open PR targeting the same base. Because git hooks can be bypassed with `--no-verify`, add this check to your branch-protection **required status checks** for enforcement that cannot be skipped.
498
+ - **Edit-time protection across open PRs (`COLLAB_PR_CLAIMS=1`, opt-in)**: Normally locks are released on push. With this enabled, the files changed on the pushed branch are instead **retained as persistent claims** until that branch is merged or deleted on the remote. Because a claim is an ordinary lock, another developer who edits those files gets the **existing edit-time warning** (watcher) and **commit block** (pre-commit) immediately — not a surprise conflict at merge time. Lifecycle: claim on push → released by the client reconciler when the branch is merged/deleted → guaranteed `release_stale_claims` expiry (default 30 days) as a safety net. **Requires the Supabase migration** (re-run [`supabase/schema.sql`](supabase/schema.sql); the columns/RPCs are added idempotently, and the runtime degrades to today's behavior if absent). Known limits: one owner per file (last push wins if you claim the same file from two branches); squash-merge relies on "delete branch on merge"; a PR closed without deleting its branch falls to the expiry.
485
499
 
486
500
  ### Dashboard
487
501
 
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Chart helpers for the Collaborative Lock Dashboard.
3
+ * Loaded in the browser (global DashboardCharts) and in Jest (module.exports).
4
+ *
5
+ * Uses Chart.js (loaded via CDN) for lock activity timeline visualization.
6
+ */
7
+ (function (root, factory) {
8
+ var api = factory();
9
+ if (typeof module === "object" && module.exports) {
10
+ module.exports = api;
11
+ } else {
12
+ root.DashboardCharts = api;
13
+ }
14
+ })(typeof globalThis !== "undefined" ? globalThis : this, function () {
15
+ // ---------------------------------------------------------------------------
16
+ // Constants
17
+ // ---------------------------------------------------------------------------
18
+
19
+ var CHART_COLORS = {
20
+ acquisitions: "rgba(79, 70, 229, 0.75)", // primary
21
+ acquisitionsBorder: "rgba(79, 70, 229, 1)",
22
+ releases: "rgba(34, 197, 94, 0.75)", // green
23
+ releasesBorder: "rgba(34, 197, 94, 1)",
24
+ grid: "rgba(226, 232, 240, 0.6)",
25
+ text: "#64748b",
26
+ };
27
+
28
+ /**
29
+ * Compute bucket label and boundaries for a time range.
30
+ *
31
+ * @param {string} range "1h" | "24h" | "7d"
32
+ * @returns {{ bucketMs: number, bucketLabel: string, totalBuckets: number }}
33
+ */
34
+ function _bucketConfig(range) {
35
+ switch (range) {
36
+ case "1h":
37
+ return { bucketMs: 5 * 60 * 1000, bucketLabel: "5m", totalBuckets: 12 };
38
+ case "7d":
39
+ return {
40
+ bucketMs: 24 * 60 * 60 * 1000,
41
+ bucketLabel: "1d",
42
+ totalBuckets: 7,
43
+ };
44
+ case "24h":
45
+ default:
46
+ return {
47
+ bucketMs: 60 * 60 * 1000,
48
+ bucketLabel: "1h",
49
+ totalBuckets: 24,
50
+ };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Bucket history rows into time-series counts.
56
+ *
57
+ * @param {Array<object>} history Array of lock history rows.
58
+ * @param {string} range "1h" | "24h" | "7d"
59
+ * @returns {{ labels: Array<string>, acquired: Array<number>, released: Array<number> }}
60
+ */
61
+ function buildTimelineData(history, range) {
62
+ var cfg = _bucketConfig(range);
63
+ var now = Date.now();
64
+ var start = now - cfg.totalBuckets * cfg.bucketMs;
65
+
66
+ // Initialize buckets
67
+ var labels = [];
68
+ var acquired = [];
69
+ var released = [];
70
+ for (var i = 0; i < cfg.totalBuckets; i++) {
71
+ var bucketTime = start + i * cfg.bucketMs;
72
+ labels.push(_formatBucketLabel(bucketTime, range));
73
+ acquired.push(0);
74
+ released.push(0);
75
+ }
76
+
77
+ // Fill buckets from history data
78
+ (history || []).forEach(function (row) {
79
+ if (row.acquired_at) {
80
+ var acqTs = new Date(row.acquired_at).getTime();
81
+ var acqIdx = Math.floor((acqTs - start) / cfg.bucketMs);
82
+ if (acqIdx >= 0 && acqIdx < cfg.totalBuckets) {
83
+ acquired[acqIdx]++;
84
+ }
85
+ }
86
+ if (row.released_at) {
87
+ var relTs = new Date(row.released_at).getTime();
88
+ var relIdx = Math.floor((relTs - start) / cfg.bucketMs);
89
+ if (relIdx >= 0 && relIdx < cfg.totalBuckets) {
90
+ released[relIdx]++;
91
+ }
92
+ }
93
+ });
94
+
95
+ return { labels: labels, acquired: acquired, released: released };
96
+ }
97
+
98
+ /**
99
+ * Format a bucket timestamp into a human-readable label.
100
+ */
101
+ function _formatBucketLabel(ts, range) {
102
+ var d = new Date(ts);
103
+ switch (range) {
104
+ case "1h":
105
+ return (
106
+ String(d.getHours()).padStart(2, "0") +
107
+ ":" +
108
+ String(d.getMinutes()).padStart(2, "0")
109
+ );
110
+ case "7d":
111
+ return d.toLocaleDateString([], { month: "short", day: "numeric" });
112
+ case "24h":
113
+ default:
114
+ return String(d.getHours()).padStart(2, "0") + ":00";
115
+ }
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Chart initialization
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /**
123
+ * Create or return an existing Chart.js bar chart for lock activity.
124
+ *
125
+ * @param {string} canvasId ID of the <canvas> element.
126
+ * @returns {object|null} Chart.js instance, or null if Chart.js unavailable.
127
+ */
128
+ function initActivityChart(canvasId) {
129
+ if (typeof document === "undefined") return null;
130
+
131
+ var ChartCtor = (typeof window !== "undefined" && window.Chart) || null;
132
+ if (!ChartCtor) return null;
133
+
134
+ var canvas = document.getElementById(canvasId);
135
+ if (!canvas) return null;
136
+
137
+ // Destroy existing chart if re-initializing
138
+ var existing = ChartCtor.getChart(canvas);
139
+ if (existing) existing.destroy();
140
+
141
+ return new ChartCtor(canvas, {
142
+ type: "bar",
143
+ data: {
144
+ labels: [],
145
+ datasets: [
146
+ {
147
+ label: "Acquired",
148
+ data: [],
149
+ backgroundColor: CHART_COLORS.acquisitions,
150
+ borderColor: CHART_COLORS.acquisitionsBorder,
151
+ borderWidth: 1,
152
+ borderRadius: 4,
153
+ },
154
+ {
155
+ label: "Released",
156
+ data: [],
157
+ backgroundColor: CHART_COLORS.releases,
158
+ borderColor: CHART_COLORS.releasesBorder,
159
+ borderWidth: 1,
160
+ borderRadius: 4,
161
+ },
162
+ ],
163
+ },
164
+ options: {
165
+ animation: false,
166
+ responsive: true,
167
+ maintainAspectRatio: false,
168
+ interaction: {
169
+ mode: "index",
170
+ intersect: false,
171
+ },
172
+ hover: {
173
+ mode: "index",
174
+ intersect: false,
175
+ },
176
+ plugins: {
177
+ legend: {
178
+ labels: {
179
+ usePointStyle: true,
180
+ padding: 8,
181
+ color: CHART_COLORS.text,
182
+ font: { size: 12, weight: "600" },
183
+ },
184
+ },
185
+ tooltip: {
186
+ backgroundColor: "#1e293b",
187
+ titleFont: { size: 13, weight: "700" },
188
+ bodyFont: { size: 12 },
189
+ padding: 10,
190
+ cornerRadius: 8,
191
+ },
192
+ },
193
+ scales: {
194
+ x: {
195
+ grid: { color: CHART_COLORS.grid },
196
+ ticks: {
197
+ color: CHART_COLORS.text,
198
+ font: { size: 11 },
199
+ maxRotation: 45,
200
+ },
201
+ },
202
+ y: {
203
+ beginAtZero: true,
204
+ grid: { color: CHART_COLORS.grid },
205
+ ticks: {
206
+ color: CHART_COLORS.text,
207
+ font: { size: 11 },
208
+ stepSize: 1,
209
+ },
210
+ title: {
211
+ display: true,
212
+ text: "Lock Events",
213
+ color: CHART_COLORS.text,
214
+ font: { size: 11, weight: "600" },
215
+ },
216
+ },
217
+ },
218
+ },
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Update an existing Chart.js instance with new timeline data.
224
+ *
225
+ * @param {object} chart Chart.js instance.
226
+ * @param {Array<object>} history Array of lock history rows.
227
+ * @param {string} range "1h" | "24h" | "7d"
228
+ */
229
+ function updateActivityChart(chart, history, range) {
230
+ if (!chart) return;
231
+
232
+ var data = buildTimelineData(history, range);
233
+ chart.data.labels = data.labels;
234
+ chart.data.datasets[0].data = data.acquired;
235
+ chart.data.datasets[1].data = data.released;
236
+ chart.update("none"); // no animation on data update for performance
237
+ }
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // Public API
241
+ // ---------------------------------------------------------------------------
242
+
243
+ return {
244
+ initActivityChart: initActivityChart,
245
+ updateActivityChart: updateActivityChart,
246
+ buildTimelineData: buildTimelineData,
247
+ };
248
+ });