awgit 0.1.0__tar.gz → 0.2.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 (32) hide show
  1. {awgit-0.1.0 → awgit-0.2.0}/PKG-INFO +2 -2
  2. {awgit-0.1.0 → awgit-0.2.0}/README.md +1 -1
  3. {awgit-0.1.0 → awgit-0.2.0}/awgit/cli.py +55 -7
  4. {awgit-0.1.0 → awgit-0.2.0}/awgit/leases.py +49 -5
  5. {awgit-0.1.0 → awgit-0.2.0}/awgit.egg-info/PKG-INFO +2 -2
  6. {awgit-0.1.0 → awgit-0.2.0}/pyproject.toml +1 -1
  7. {awgit-0.1.0 → awgit-0.2.0}/LICENSE +0 -0
  8. {awgit-0.1.0 → awgit-0.2.0}/awgit/__init__.py +0 -0
  9. {awgit-0.1.0 → awgit-0.2.0}/awgit/bodies.py +0 -0
  10. {awgit-0.1.0 → awgit-0.2.0}/awgit/bridge.py +0 -0
  11. {awgit-0.1.0 → awgit-0.2.0}/awgit/capture.py +0 -0
  12. {awgit-0.1.0 → awgit-0.2.0}/awgit/data_root.py +0 -0
  13. {awgit-0.1.0 → awgit-0.2.0}/awgit/diff.py +0 -0
  14. {awgit-0.1.0 → awgit-0.2.0}/awgit/hooks/chain.sh +0 -0
  15. {awgit-0.1.0 → awgit-0.2.0}/awgit/hooks/post-commit.d/vcs-capture +0 -0
  16. {awgit-0.1.0 → awgit-0.2.0}/awgit/hooks/pre-commit.d/vcs-lease-check +0 -0
  17. {awgit-0.1.0 → awgit-0.2.0}/awgit/identity.py +0 -0
  18. {awgit-0.1.0 → awgit-0.2.0}/awgit/ledger.py +0 -0
  19. {awgit-0.1.0 → awgit-0.2.0}/awgit/mcp.py +0 -0
  20. {awgit-0.1.0 → awgit-0.2.0}/awgit/merge.py +0 -0
  21. {awgit-0.1.0 → awgit-0.2.0}/awgit/nodeid.py +0 -0
  22. {awgit-0.1.0 → awgit-0.2.0}/awgit/oplog.py +0 -0
  23. {awgit-0.1.0 → awgit-0.2.0}/awgit/parser.py +0 -0
  24. {awgit-0.1.0 → awgit-0.2.0}/awgit/schema.py +0 -0
  25. {awgit-0.1.0 → awgit-0.2.0}/awgit/sync.py +0 -0
  26. {awgit-0.1.0 → awgit-0.2.0}/awgit.egg-info/SOURCES.txt +0 -0
  27. {awgit-0.1.0 → awgit-0.2.0}/awgit.egg-info/dependency_links.txt +0 -0
  28. {awgit-0.1.0 → awgit-0.2.0}/awgit.egg-info/entry_points.txt +0 -0
  29. {awgit-0.1.0 → awgit-0.2.0}/awgit.egg-info/requires.txt +0 -0
  30. {awgit-0.1.0 → awgit-0.2.0}/awgit.egg-info/top_level.txt +0 -0
  31. {awgit-0.1.0 → awgit-0.2.0}/setup.cfg +0 -0
  32. {awgit-0.1.0 → awgit-0.2.0}/tests/test_awgit_standalone.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awgit
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Aither World-Graph git — semantic version control on top of git: edit-ops keyed on stable node ids, content-addressed bodies, differential sync.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -52,7 +52,7 @@ what changed, never the snapshot.
52
52
  ## Install
53
53
 
54
54
  ```bash
55
- pip install awgit # from PyPI (when published)
55
+ pip install awgit
56
56
  # or from source:
57
57
  git clone https://github.com/aitherium/awgit && cd awgit
58
58
  pip install -e .
@@ -41,7 +41,7 @@ what changed, never the snapshot.
41
41
  ## Install
42
42
 
43
43
  ```bash
44
- pip install awgit # from PyPI (when published)
44
+ pip install awgit
45
45
  # or from source:
46
46
  git clone https://github.com/aitherium/awgit && cd awgit
47
47
  pip install -e .
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  import argparse
17
17
  import os
18
+ import platform
18
19
  import subprocess
19
20
  import sys
20
21
  from pathlib import Path
@@ -28,11 +29,38 @@ from awgit.oplog import OpLog
28
29
 
29
30
 
30
31
  def _actor(args: argparse.Namespace) -> str:
31
- return (
32
- getattr(args, "actor", None)
33
- or os.environ.get("AITHER_ACTOR")
34
- or "unknown"
35
- )
32
+ """Who is committing. MUST differ per concurrent agent, or leases are useless.
33
+
34
+ Explicit forms win, then one is DERIVED. Deriving is the point: with only
35
+ ``AITHER_ACTOR`` this returned "unknown", and ``lease-check`` rejects an
36
+ unknown actor — so turning enforcement on would have blocked every commit on
37
+ the box until every session remembered to export a variable. A guard that can
38
+ only be switched on by breaking the machine never gets switched on, and it
39
+ never was: measured 2026-08-09, ``awgit lease list`` had never returned a
40
+ single lease while a concurrent session overwrote one file four times, the
41
+ last overwrite silently reverting a fix already committed to origin.
42
+
43
+ ``CLAUDE_CODE_SESSION_ID`` is the right derivation: the harness sets it per
44
+ session, it is stable for that session's life, and it is inherited by the git
45
+ hook's subprocess (verified). A per-PID value would NOT do — every ``awgit``
46
+ invocation is a new process, so a lease taken by one command would not match
47
+ the next.
48
+
49
+ ``user@host`` is the last resort, for humans and other tooling. It is
50
+ deliberately not what agents get: every session here shares one OS account
51
+ and one GitHub identity, so a shared actor makes session A's lease cover
52
+ session B's commit — all of the friction, none of the protection.
53
+ """
54
+ explicit = getattr(args, "actor", None) or os.environ.get("AITHER_ACTOR")
55
+ if explicit:
56
+ return explicit
57
+ session = os.environ.get("CLAUDE_CODE_SESSION_ID")
58
+ if session:
59
+ return f"claude:{session}"
60
+ user = os.environ.get("USERNAME") or os.environ.get("USER")
61
+ if user:
62
+ return f"{user}@{platform.node() or 'localhost'}"
63
+ return "unknown"
36
64
 
37
65
 
38
66
  def _cmd_capture(args: argparse.Namespace) -> int:
@@ -119,9 +147,26 @@ def _cmd_lease(args: argparse.Namespace) -> int:
119
147
  cmd = args.lease_cmd
120
148
  who = _actor(args)
121
149
  if cmd == "acquire":
150
+ targets = list(args.targets or [])
151
+ if getattr(args, "staged", False):
152
+ # Lease exactly what the gate will check. Without this, widening
153
+ # coverage beyond .py (D-1880) would have made a routine commit a
154
+ # per-file chore, and a gate that heavy gets routed around instead
155
+ # of satisfied.
156
+ from awgit.leases import is_guarded
157
+
158
+ repo = Path(os.environ.get("VCS_REPO_ROOT", os.getcwd()))
159
+ targets += [f for f in _staged_files(repo) if is_guarded(f)]
160
+ targets = sorted(set(targets))
161
+ if not targets:
162
+ print("vcs: nothing staged that the gate guards — no leases needed")
163
+ return 0
164
+ if not targets:
165
+ print("vcs: acquire needs targets (or --staged)", file=sys.stderr)
166
+ return 1
122
167
  try:
123
168
  leases = registry.acquire(
124
- who, args.targets, ttl_sec=args.ttl, reason=args.reason
169
+ who, targets, ttl_sec=args.ttl, reason=args.reason
125
170
  )
126
171
  except LeaseConflictError as exc:
127
172
  print(f"vcs: {exc}", file=sys.stderr)
@@ -376,7 +421,10 @@ def main(argv: Optional[List[str]] = None) -> int:
376
421
  p_lease = sub.add_parser("lease", help="lease registry operations")
377
422
  lsub = p_lease.add_subparsers(dest="lease_cmd", required=True)
378
423
  p_la = lsub.add_parser("acquire", help="acquire leases for targets")
379
- p_la.add_argument("targets", nargs="+")
424
+ p_la.add_argument("targets", nargs="*")
425
+ p_la.add_argument("--staged", action="store_true",
426
+ help="also lease every STAGED file the gate guards "
427
+ "(the one-command way to satisfy the pre-commit gate)")
380
428
  p_la.add_argument("--ttl", type=int, default=300)
381
429
  p_la.add_argument("--reason", default="")
382
430
  p_la.add_argument("--actor", default=None)
@@ -243,16 +243,60 @@ class LeaseRegistry:
243
243
  return [lz for lz in self._leases.values() if lz.actor == actor]
244
244
 
245
245
 
246
+ # Files worth guarding: the ones two agents plausibly edit at the same time.
247
+ # Widened from ".py only" on 2026-08-09 (D-1880). The old scope let a `.yml`
248
+ # commit print "lease-check OK" while checking NOTHING, so compose files,
249
+ # workflows, skills and .mcp.json were exactly as clobberable as before the
250
+ # gate existed — and that same day edits to docker-compose.aitheros.yml and
251
+ # concurrent-safe-git/SKILL.md had to be hand-restored after a concurrent
252
+ # overwrite, while the .py the gate did cover was overwritten four times.
253
+ GUARDED_SUFFIXES = (
254
+ ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", # source
255
+ ".yml", ".yaml", ".toml", ".json", ".ini", ".cfg", # config
256
+ ".sh", ".bash", ".ps1", ".psm1", # scripts
257
+ ".md", # rules, skills, docs
258
+ ".sql", ".proto", ".env",
259
+ )
260
+ # Bulk content nobody races on. Guarding these would make a routine content
261
+ # commit need dozens of leases, and a gate that heavy gets ROUTED AROUND rather
262
+ # than satisfied — the exact failure that produced this repo's per-file-ignores.
263
+ UNGUARDED_PREFIXES = (
264
+ "AitherOS/apps/AitherVeil/content/", # blog posts, one author at a time
265
+ "AitherOS/Library/", # generated training/runtime data
266
+ ".DELIVERABLES/", # published artifacts
267
+ ".RESEARCH/INTAKE/", # dossiers
268
+ "TECH_DEBT_ARCHIVE", # frozen
269
+ )
270
+
271
+
272
+ def is_guarded(path: str) -> bool:
273
+ """Does the lease gate care about this path?
274
+
275
+ Extension-based with a bulk-content escape, rather than "everything": the
276
+ trade is deliberate. Covering every path means a 50-file commit needs 50
277
+ leases; covering none is where this started.
278
+ """
279
+ p = (path or "").replace("\\", "/").strip()
280
+ if not p:
281
+ return False
282
+ if p.startswith(UNGUARDED_PREFIXES):
283
+ return False
284
+ return p.endswith(GUARDED_SUFFIXES)
285
+
286
+
246
287
  def coverage_gap(
247
288
  changed_files: List[str],
248
289
  actor: str,
249
290
  data_root: Optional[Path] = None,
250
291
  ) -> List[str]:
251
- """Changed Python files NOT covered by the actor's active path-leases.
292
+ """Changed GUARDED files not covered by the actor's active path-leases.
293
+
294
+ The pre-commit gate's primitive: a commit touching a guarded file with no
295
+ active path-lease is an unleased edit — rejected when leases are enforced
296
+ (VCS_LEASES_ENFORCE=1), recorded as ``leased=false`` otherwise.
252
297
 
253
- The pre-commit gate's primitive: a commit touching a Python file with no
254
- active path-lease is an unleased edit — a visible risk flag when leases
255
- are enforced (VCS_LEASES_ENFORCE=1), recorded as ``leased=false`` otherwise.
298
+ ``awgit lease acquire --staged`` takes leases on exactly this set, so
299
+ complying is one command rather than a per-file chore.
256
300
  """
257
301
  registry = LeaseRegistry(data_root=data_root)
258
302
  registry.sweep_expired()
@@ -261,4 +305,4 @@ def coverage_gap(
261
305
  for lz in registry.active_leases()
262
306
  if lz.actor == actor and lz.kind == "path"
263
307
  }
264
- return [f for f in changed_files if f.endswith(".py") and f not in covered]
308
+ return [f for f in changed_files if is_guarded(f) and f not in covered]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awgit
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Aither World-Graph git — semantic version control on top of git: edit-ops keyed on stable node ids, content-addressed bodies, differential sync.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -52,7 +52,7 @@ what changed, never the snapshot.
52
52
  ## Install
53
53
 
54
54
  ```bash
55
- pip install awgit # from PyPI (when published)
55
+ pip install awgit
56
56
  # or from source:
57
57
  git clone https://github.com/aitherium/awgit && cd awgit
58
58
  pip install -e .
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "awgit"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Aither World-Graph git — semantic version control on top of git: edit-ops keyed on stable node ids, content-addressed bodies, differential sync."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
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