awgit 1.1.2__tar.gz → 1.3.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 (71) hide show
  1. {awgit-1.1.2/awgit.egg-info → awgit-1.3.0}/PKG-INFO +68 -37
  2. awgit-1.1.2/PKG-INFO → awgit-1.3.0/README.md +63 -49
  3. {awgit-1.1.2 → awgit-1.3.0}/awgit/cli.py +187 -5
  4. {awgit-1.1.2 → awgit-1.3.0}/awgit/guard.py +117 -0
  5. awgit-1.3.0/awgit/lease_requests.py +171 -0
  6. awgit-1.3.0/awgit/outcomes.py +250 -0
  7. {awgit-1.1.2 → awgit-1.3.0}/awgit/schema.py +92 -0
  8. {awgit-1.1.2 → awgit-1.3.0}/awgit/worktree.py +1 -0
  9. awgit-1.1.2/README.md → awgit-1.3.0/awgit.egg-info/PKG-INFO +80 -34
  10. {awgit-1.1.2 → awgit-1.3.0}/awgit.egg-info/SOURCES.txt +5 -0
  11. {awgit-1.1.2 → awgit-1.3.0}/awgit.egg-info/requires.txt +0 -3
  12. {awgit-1.1.2 → awgit-1.3.0}/pyproject.toml +11 -7
  13. awgit-1.3.0/tests/test_lease_release_accepts_paths.py +142 -0
  14. awgit-1.3.0/tests/test_lease_requests.py +156 -0
  15. awgit-1.3.0/tests/test_outcome_tracking.py +410 -0
  16. {awgit-1.1.2 → awgit-1.3.0}/LICENSE +0 -0
  17. {awgit-1.1.2 → awgit-1.3.0}/awgit/__init__.py +0 -0
  18. {awgit-1.1.2 → awgit-1.3.0}/awgit/absorb.py +0 -0
  19. {awgit-1.1.2 → awgit-1.3.0}/awgit/bodies.py +0 -0
  20. {awgit-1.1.2 → awgit-1.3.0}/awgit/bridge.py +0 -0
  21. {awgit-1.1.2 → awgit-1.3.0}/awgit/capture.py +0 -0
  22. {awgit-1.1.2 → awgit-1.3.0}/awgit/changeid.py +0 -0
  23. {awgit-1.1.2 → awgit-1.3.0}/awgit/code.py +0 -0
  24. {awgit-1.1.2 → awgit-1.3.0}/awgit/commands.py +0 -0
  25. {awgit-1.1.2 → awgit-1.3.0}/awgit/data_root.py +0 -0
  26. {awgit-1.1.2 → awgit-1.3.0}/awgit/diff.py +0 -0
  27. {awgit-1.1.2 → awgit-1.3.0}/awgit/evidence.py +0 -0
  28. {awgit-1.1.2 → awgit-1.3.0}/awgit/git.py +0 -0
  29. {awgit-1.1.2 → awgit-1.3.0}/awgit/graph.py +0 -0
  30. {awgit-1.1.2 → awgit-1.3.0}/awgit/hooks/chain.sh +0 -0
  31. {awgit-1.1.2 → awgit-1.3.0}/awgit/hooks/post-commit.d/vcs-capture +0 -0
  32. {awgit-1.1.2 → awgit-1.3.0}/awgit/hooks/pre-commit.d/vcs-lease-check +0 -0
  33. {awgit-1.1.2 → awgit-1.3.0}/awgit/hooks/pre-commit.d/vcs-mass-delete-guard +0 -0
  34. {awgit-1.1.2 → awgit-1.3.0}/awgit/hooks/pre-push.d/ci-gate-parity +0 -0
  35. {awgit-1.1.2 → awgit-1.3.0}/awgit/hooks/prepare-commit-msg.d/awgit-change-id +0 -0
  36. {awgit-1.1.2 → awgit-1.3.0}/awgit/identity.py +0 -0
  37. {awgit-1.1.2 → awgit-1.3.0}/awgit/lazy.py +0 -0
  38. {awgit-1.1.2 → awgit-1.3.0}/awgit/leases.py +0 -0
  39. {awgit-1.1.2 → awgit-1.3.0}/awgit/ledger.py +0 -0
  40. {awgit-1.1.2 → awgit-1.3.0}/awgit/mcp.py +0 -0
  41. {awgit-1.1.2 → awgit-1.3.0}/awgit/merge.py +0 -0
  42. {awgit-1.1.2 → awgit-1.3.0}/awgit/nodeid.py +0 -0
  43. {awgit-1.1.2 → awgit-1.3.0}/awgit/oplog.py +0 -0
  44. {awgit-1.1.2 → awgit-1.3.0}/awgit/owners.py +0 -0
  45. {awgit-1.1.2 → awgit-1.3.0}/awgit/parser.py +0 -0
  46. {awgit-1.1.2 → awgit-1.3.0}/awgit/plugins.py +0 -0
  47. {awgit-1.1.2 → awgit-1.3.0}/awgit/prove.py +0 -0
  48. {awgit-1.1.2 → awgit-1.3.0}/awgit/push.py +0 -0
  49. {awgit-1.1.2 → awgit-1.3.0}/awgit/repowise_parser.py +0 -0
  50. {awgit-1.1.2 → awgit-1.3.0}/awgit/review.py +0 -0
  51. {awgit-1.1.2 → awgit-1.3.0}/awgit/stack.py +0 -0
  52. {awgit-1.1.2 → awgit-1.3.0}/awgit/staging.py +0 -0
  53. {awgit-1.1.2 → awgit-1.3.0}/awgit/staging_selftest.py +0 -0
  54. {awgit-1.1.2 → awgit-1.3.0}/awgit/sync.py +0 -0
  55. {awgit-1.1.2 → awgit-1.3.0}/awgit/tabular.py +0 -0
  56. {awgit-1.1.2 → awgit-1.3.0}/awgit.egg-info/dependency_links.txt +0 -0
  57. {awgit-1.1.2 → awgit-1.3.0}/awgit.egg-info/entry_points.txt +0 -0
  58. {awgit-1.1.2 → awgit-1.3.0}/awgit.egg-info/top_level.txt +0 -0
  59. {awgit-1.1.2 → awgit-1.3.0}/setup.cfg +0 -0
  60. {awgit-1.1.2 → awgit-1.3.0}/tests/test_absorb_routes_by_node.py +0 -0
  61. {awgit-1.1.2 → awgit-1.3.0}/tests/test_awgit_standalone.py +0 -0
  62. {awgit-1.1.2 → awgit-1.3.0}/tests/test_changeid_survives_history_rewrite.py +0 -0
  63. {awgit-1.1.2 → awgit-1.3.0}/tests/test_documented_commands_run.py +0 -0
  64. {awgit-1.1.2 → awgit-1.3.0}/tests/test_lazy_clone_is_verified.py +0 -0
  65. {awgit-1.1.2 → awgit-1.3.0}/tests/test_multilang_identity.py +0 -0
  66. {awgit-1.1.2 → awgit-1.3.0}/tests/test_owners_and_proof.py +0 -0
  67. {awgit-1.1.2 → awgit-1.3.0}/tests/test_push_is_the_pull_request.py +0 -0
  68. {awgit-1.1.2 → awgit-1.3.0}/tests/test_restack_repairs_orphans.py +0 -0
  69. {awgit-1.1.2 → awgit-1.3.0}/tests/test_review_threads_survive_moves.py +0 -0
  70. {awgit-1.1.2 → awgit-1.3.0}/tests/test_tabular.py +0 -0
  71. {awgit-1.1.2 → awgit-1.3.0}/tests/test_worktree_zombie_detection.py +0 -0
@@ -1,16 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awgit
3
- Version: 1.1.2
3
+ Version: 1.3.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
+ Project-URL: Homepage, https://github.com/Aitherium/awgit
7
+ Project-URL: Documentation, https://aitherium.github.io/awgit/
8
+ Project-URL: Repository, https://github.com/Aitherium/awgit.git
9
+ Project-URL: Issues, https://github.com/Aitherium/awgit/issues
6
10
  Requires-Python: >=3.10
7
11
  Description-Content-Type: text/markdown
8
12
  License-File: LICENSE
9
13
  Requires-Dist: httpx>=0.25.0
10
14
  Provides-Extra: multilang
11
15
  Requires-Dist: repowise>=0.31.0; extra == "multilang"
12
- Provides-Extra: tabular
13
- Requires-Dist: pyarrow>=15.0.0; extra == "tabular"
14
16
  Dynamic: license-file
15
17
 
16
18
  # awgit — the git that knows a function from a line
@@ -205,42 +207,8 @@ awgit ledger --sha <sha> # who changed what, under a verified identity
205
207
  awgit evidence # the measurable claim, from your own op-log
206
208
  awgit bodies --get <sha> # read a body from the content-addressed store
207
209
  awgit dedupe --scan <trees> # quantify duplication; --reclaim to hard-link
208
- awgit data diff <a> <b> --key id # ROW-level diff of CSV/TSV/parquet
209
210
  ```
210
211
 
211
- ### Data files get the same treatment as code
212
-
213
- A line diff is useless on a table: sort it and every line "changed"; reorder two
214
- rows and a review drowns. So a row gets the same pair a function gets — an
215
- **identity** and a **content address**:
216
-
217
- ```
218
- row identity = H(the --key columns) # which row is this?
219
- row content = H(every column) # has it changed?
220
- ```
221
-
222
- The diff is then set algebra on identity, so rows can be reordered freely and
223
- nothing is reported:
224
-
225
- ```bash
226
- awgit data diff old.csv new.csv --key id # 1 added, 1 removed, 1 modified
227
- awgit data diff old.csv new.csv --key id --json # before/after per modified row
228
- ```
229
-
230
- Without `--key` there is no identity, so it degrades to a content set-diff and
231
- **says so** — every edit reads as an add plus a remove, and `modified` stays
232
- empty rather than being guessed at. A key column that exists in neither table is
233
- an error, not an empty result: silently keying on a missing column would report
234
- every row as added *and* removed, which looks exactly like data loss.
235
-
236
- CSV and TSV need nothing beyond the stdlib. Parquet needs the optional extra:
237
- `pip install awgit[tabular]`.
238
-
239
- *The two-hash row model is adapted from [Oxen](https://github.com/oxen-ai/Oxen)
240
- (Apache-2.0). No Oxen code is vendored, and awgit's byte encoding deliberately
241
- differs — it length-prefixes each field so that two different rows cannot share
242
- a content address — so hashes are not comparable between the two tools.*
243
-
244
212
  - **Merge** at node granularity: disjoint node sets merge clean by
245
213
  construction, and a genuine collision escalates naming the exact function
246
214
  (`awgit merge-preview`, `awgit merge-conflicts`, `awgit resolve-conflict`).
@@ -296,3 +264,66 @@ cd awgit && python -m pytest tests/ -q
296
264
  ## License
297
265
 
298
266
  Apache-2.0. Built by Aitherium — the git that scales to agents.
267
+
268
+ ---
269
+
270
+ <!-- aither-ecosystem:start GENERATED from the ecosystem registry. Edits here are overwritten; change the registry instead. -->
271
+
272
+ ## The aw family
273
+
274
+ Standalone tools that share one idea: **replace something you would otherwise have to _trust_ with something you can _check_.**
275
+
276
+ Each installs on its own, works offline, and needs no account.
277
+
278
+ | | instead of trusting | you check |
279
+ |---|---|---|
280
+ | [awdk](https://github.com/Aitherium/awdk) | a framework's idea of how your agents should run | one loop you can read, pointed at a backend you already pay for |
281
+ | [awskills](https://github.com/Aitherium/awskills) | that an agent knows your procedure | the procedure written down, versioned, and loadable by any agent |
282
+ | [awm](https://github.com/Aitherium/awm) | that memory stayed in its lane | tenant:user:project scopes, so a write cannot cross a boundary |
283
+ | [awnode](https://github.com/Aitherium/awnode) | a vendor's cloud with every prompt | a local gateway routing to backends you chose |
284
+ | [awgraph](https://github.com/Aitherium/awgraph) | that grep found everything | an AST + tree-sitter call graph an agent can traverse |
285
+ | **awgit** _(you are here)_ | that no one else is editing this file | a lease, refused at commit time if you do not hold it |
286
+ | [awseal](https://github.com/Aitherium/awseal) | that the artifact came from who you think | an Ed25519 seal — the key that verifies is not the key that forges |
287
+ | [awshare](https://github.com/Aitherium/awshare) | that the download is intact | content-addressed bundles, verified on fetch |
288
+ | [awnest](https://github.com/Aitherium/awnest) | that there is a person on the other end | a verdict with evidence, where "we could not tell" is not "yes" |
289
+ | [awnboard](https://github.com/Aitherium/awnboard) | a share link anyone who sees it can use | an invitation addressed to one person, for one gate, revocable |
290
+ | [awnix](https://github.com/Aitherium/awnix) | that the box is what you left it as | an immutable image you built, with atomic rollback |
291
+ | [awrecover](https://github.com/Aitherium/awrecover) | that the restore worked | a restore that fully lands or does not land at all |
292
+ | [awrelay](https://github.com/Aitherium/awrelay) | a SaaS in the middle of your agents | findings, alerts and coordination over your own transport |
293
+ | [awmail](https://github.com/Aitherium/awmail) | a mailbox somebody else can read | mail your agents send and receive over your own server |
294
+ | [awfind](https://github.com/Aitherium/awfind) | one vendor's idea of the web | results from whichever providers you configured |
295
+ | [awbrowse](https://github.com/Aitherium/awbrowse) | that the page said what you were told | the render, the DOM and the requests it made |
296
+ | [aitherkvcache](https://github.com/Aitherium/aitherkvcache) | a vendor's quantisation defaults | sub-byte KV cache kernels you can benchmark yourself |
297
+ | [AitherZero](https://github.com/Aitherium/AitherZero) | a pile of scripts nobody has numbered | numbered, discoverable automation with declarative playbooks |
298
+ | [AitherConnect](https://github.com/Aitherium/AitherConnect) | what a page tells your browser to do | a federated search and desktop bridge you host |
299
+
300
+ [**awnix**](https://github.com/Aitherium/awnix) is the ground floor — A Linux you can hand to an agent — immutable base, capabilities included.
301
+
302
+ ## The Aitherium ecosystem
303
+
304
+ Every repository here is public. Each publishes an `aither-manifest.json` beside its page, so any surface can read every sibling's — the network is browsable from any node in it.
305
+
306
+ | repo | what it is | pages |
307
+ |---|---|---|
308
+ | [awdk](https://github.com/Aitherium/awdk) | Build AI agent fleets — 3 lines, any backend, local or cloud | [docs](https://aitherium.github.io/awdk/) |
309
+ | [awskills](https://github.com/Aitherium/awskills) | Portable agent skills — self-contained procedures an agent loads on demand | [docs](https://aitherium.github.io/awskills/) |
310
+ | [awm](https://github.com/Aitherium/awm) | A portable, scoped agent memory | [docs](https://aitherium.github.io/awm/) |
311
+ | [awnode](https://github.com/Aitherium/awnode) | A lightweight local gateway — bridges your apps to the AI backends you chose | [docs](https://aitherium.github.io/awnode/) |
312
+ | [awrun](https://github.com/Aitherium/awrun) | A priority-aware queue and dispatcher for agentic runs and ad-hoc CI builds | [docs](https://aitherium.github.io/awrun/) |
313
+ | [awgraph](https://github.com/Aitherium/awgraph) | A semantic code graph for agents — AST + tree-sitter, call graphs | [docs](https://aitherium.github.io/awgraph/) |
314
+ | **awgit** _(you are here)_ | Semantic version control on top of git — edit-ops and leases | [docs](https://aitherium.github.io/awgit/) |
315
+ | [awseal](https://github.com/Aitherium/awseal) | Sign an artifact so a stranger can verify it | [docs](https://aitherium.github.io/awseal/) |
316
+ | [awshare](https://github.com/Aitherium/awshare) | Publish an artifact and fetch it back verified | [docs](https://aitherium.github.io/awshare/) |
317
+ | [awnest](https://github.com/Aitherium/awnest) | Prove there is a human before you let them into the nest | — |
318
+ | [awnboard](https://github.com/Aitherium/awnboard) | A front gate you can put in front of anything, and hand someone the key to | — |
319
+ | [awnix](https://github.com/Aitherium/awnix) | A Linux you can hand to an agent — immutable base, capabilities included | [docs](https://aitherium.github.io/awnix/) |
320
+ | [awrecover](https://github.com/Aitherium/awrecover) | Labelled snapshots with an all-or-nothing restore | [docs](https://aitherium.github.io/awrecover/) |
321
+ | [awrelay](https://github.com/Aitherium/awrelay) | Portable agent messaging — findings, alerts, coordination | [docs](https://aitherium.github.io/awrelay/) |
322
+ | [awmail](https://github.com/Aitherium/awmail) | Give an agent an email address — send, and actually receive | — |
323
+ | [awfind](https://github.com/Aitherium/awfind) | A portable search client — query, results, ranking | — |
324
+ | [awbrowse](https://github.com/Aitherium/awbrowse) | A portable browser client — navigate, console, network, DOM, screenshot | — |
325
+ | [aitherkvcache](https://github.com/Aitherium/aitherkvcache) | Near-optimal KV cache quantization for LLM inference — sub-byte compression | [docs](https://aitherium.github.io/aitherkvcache/) |
326
+ | [AitherZero](https://github.com/Aitherium/AitherZero) | PowerShell 7+ automation framework — numbered, self-describing scripts | [docs](https://aitherium.github.io/AitherZero/) |
327
+ | [AitherConnect](https://github.com/Aitherium/AitherConnect) | Browser extension — federated AI search, page context, and the Living OS overlay | — |
328
+
329
+ <!-- aither-ecosystem:end -->
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: awgit
3
- Version: 1.1.2
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
- License: Apache-2.0
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: httpx>=0.25.0
10
- Provides-Extra: multilang
11
- Requires-Dist: repowise>=0.31.0; extra == "multilang"
12
- Provides-Extra: tabular
13
- Requires-Dist: pyarrow>=15.0.0; extra == "tabular"
14
- Dynamic: license-file
15
-
16
1
  # awgit — the git that knows a function from a line
17
2
 
18
3
  Git has no world model. It knows which lines moved, not what a function is — so
@@ -205,42 +190,8 @@ awgit ledger --sha <sha> # who changed what, under a verified identity
205
190
  awgit evidence # the measurable claim, from your own op-log
206
191
  awgit bodies --get <sha> # read a body from the content-addressed store
207
192
  awgit dedupe --scan <trees> # quantify duplication; --reclaim to hard-link
208
- awgit data diff <a> <b> --key id # ROW-level diff of CSV/TSV/parquet
209
- ```
210
-
211
- ### Data files get the same treatment as code
212
-
213
- A line diff is useless on a table: sort it and every line "changed"; reorder two
214
- rows and a review drowns. So a row gets the same pair a function gets — an
215
- **identity** and a **content address**:
216
-
217
- ```
218
- row identity = H(the --key columns) # which row is this?
219
- row content = H(every column) # has it changed?
220
- ```
221
-
222
- The diff is then set algebra on identity, so rows can be reordered freely and
223
- nothing is reported:
224
-
225
- ```bash
226
- awgit data diff old.csv new.csv --key id # 1 added, 1 removed, 1 modified
227
- awgit data diff old.csv new.csv --key id --json # before/after per modified row
228
193
  ```
229
194
 
230
- Without `--key` there is no identity, so it degrades to a content set-diff and
231
- **says so** — every edit reads as an add plus a remove, and `modified` stays
232
- empty rather than being guessed at. A key column that exists in neither table is
233
- an error, not an empty result: silently keying on a missing column would report
234
- every row as added *and* removed, which looks exactly like data loss.
235
-
236
- CSV and TSV need nothing beyond the stdlib. Parquet needs the optional extra:
237
- `pip install awgit[tabular]`.
238
-
239
- *The two-hash row model is adapted from [Oxen](https://github.com/oxen-ai/Oxen)
240
- (Apache-2.0). No Oxen code is vendored, and awgit's byte encoding deliberately
241
- differs — it length-prefixes each field so that two different rows cannot share
242
- a content address — so hashes are not comparable between the two tools.*
243
-
244
195
  - **Merge** at node granularity: disjoint node sets merge clean by
245
196
  construction, and a genuine collision escalates naming the exact function
246
197
  (`awgit merge-preview`, `awgit merge-conflicts`, `awgit resolve-conflict`).
@@ -296,3 +247,66 @@ cd awgit && python -m pytest tests/ -q
296
247
  ## License
297
248
 
298
249
  Apache-2.0. Built by Aitherium — the git that scales to agents.
250
+
251
+ ---
252
+
253
+ <!-- aither-ecosystem:start GENERATED from the ecosystem registry. Edits here are overwritten; change the registry instead. -->
254
+
255
+ ## The aw family
256
+
257
+ Standalone tools that share one idea: **replace something you would otherwise have to _trust_ with something you can _check_.**
258
+
259
+ Each installs on its own, works offline, and needs no account.
260
+
261
+ | | instead of trusting | you check |
262
+ |---|---|---|
263
+ | [awdk](https://github.com/Aitherium/awdk) | a framework's idea of how your agents should run | one loop you can read, pointed at a backend you already pay for |
264
+ | [awskills](https://github.com/Aitherium/awskills) | that an agent knows your procedure | the procedure written down, versioned, and loadable by any agent |
265
+ | [awm](https://github.com/Aitherium/awm) | that memory stayed in its lane | tenant:user:project scopes, so a write cannot cross a boundary |
266
+ | [awnode](https://github.com/Aitherium/awnode) | a vendor's cloud with every prompt | a local gateway routing to backends you chose |
267
+ | [awgraph](https://github.com/Aitherium/awgraph) | that grep found everything | an AST + tree-sitter call graph an agent can traverse |
268
+ | **awgit** _(you are here)_ | that no one else is editing this file | a lease, refused at commit time if you do not hold it |
269
+ | [awseal](https://github.com/Aitherium/awseal) | that the artifact came from who you think | an Ed25519 seal — the key that verifies is not the key that forges |
270
+ | [awshare](https://github.com/Aitherium/awshare) | that the download is intact | content-addressed bundles, verified on fetch |
271
+ | [awnest](https://github.com/Aitherium/awnest) | that there is a person on the other end | a verdict with evidence, where "we could not tell" is not "yes" |
272
+ | [awnboard](https://github.com/Aitherium/awnboard) | a share link anyone who sees it can use | an invitation addressed to one person, for one gate, revocable |
273
+ | [awnix](https://github.com/Aitherium/awnix) | that the box is what you left it as | an immutable image you built, with atomic rollback |
274
+ | [awrecover](https://github.com/Aitherium/awrecover) | that the restore worked | a restore that fully lands or does not land at all |
275
+ | [awrelay](https://github.com/Aitherium/awrelay) | a SaaS in the middle of your agents | findings, alerts and coordination over your own transport |
276
+ | [awmail](https://github.com/Aitherium/awmail) | a mailbox somebody else can read | mail your agents send and receive over your own server |
277
+ | [awfind](https://github.com/Aitherium/awfind) | one vendor's idea of the web | results from whichever providers you configured |
278
+ | [awbrowse](https://github.com/Aitherium/awbrowse) | that the page said what you were told | the render, the DOM and the requests it made |
279
+ | [aitherkvcache](https://github.com/Aitherium/aitherkvcache) | a vendor's quantisation defaults | sub-byte KV cache kernels you can benchmark yourself |
280
+ | [AitherZero](https://github.com/Aitherium/AitherZero) | a pile of scripts nobody has numbered | numbered, discoverable automation with declarative playbooks |
281
+ | [AitherConnect](https://github.com/Aitherium/AitherConnect) | what a page tells your browser to do | a federated search and desktop bridge you host |
282
+
283
+ [**awnix**](https://github.com/Aitherium/awnix) is the ground floor — A Linux you can hand to an agent — immutable base, capabilities included.
284
+
285
+ ## The Aitherium ecosystem
286
+
287
+ Every repository here is public. Each publishes an `aither-manifest.json` beside its page, so any surface can read every sibling's — the network is browsable from any node in it.
288
+
289
+ | repo | what it is | pages |
290
+ |---|---|---|
291
+ | [awdk](https://github.com/Aitherium/awdk) | Build AI agent fleets — 3 lines, any backend, local or cloud | [docs](https://aitherium.github.io/awdk/) |
292
+ | [awskills](https://github.com/Aitherium/awskills) | Portable agent skills — self-contained procedures an agent loads on demand | [docs](https://aitherium.github.io/awskills/) |
293
+ | [awm](https://github.com/Aitherium/awm) | A portable, scoped agent memory | [docs](https://aitherium.github.io/awm/) |
294
+ | [awnode](https://github.com/Aitherium/awnode) | A lightweight local gateway — bridges your apps to the AI backends you chose | [docs](https://aitherium.github.io/awnode/) |
295
+ | [awrun](https://github.com/Aitherium/awrun) | A priority-aware queue and dispatcher for agentic runs and ad-hoc CI builds | [docs](https://aitherium.github.io/awrun/) |
296
+ | [awgraph](https://github.com/Aitherium/awgraph) | A semantic code graph for agents — AST + tree-sitter, call graphs | [docs](https://aitherium.github.io/awgraph/) |
297
+ | **awgit** _(you are here)_ | Semantic version control on top of git — edit-ops and leases | [docs](https://aitherium.github.io/awgit/) |
298
+ | [awseal](https://github.com/Aitherium/awseal) | Sign an artifact so a stranger can verify it | [docs](https://aitherium.github.io/awseal/) |
299
+ | [awshare](https://github.com/Aitherium/awshare) | Publish an artifact and fetch it back verified | [docs](https://aitherium.github.io/awshare/) |
300
+ | [awnest](https://github.com/Aitherium/awnest) | Prove there is a human before you let them into the nest | — |
301
+ | [awnboard](https://github.com/Aitherium/awnboard) | A front gate you can put in front of anything, and hand someone the key to | — |
302
+ | [awnix](https://github.com/Aitherium/awnix) | A Linux you can hand to an agent — immutable base, capabilities included | [docs](https://aitherium.github.io/awnix/) |
303
+ | [awrecover](https://github.com/Aitherium/awrecover) | Labelled snapshots with an all-or-nothing restore | [docs](https://aitherium.github.io/awrecover/) |
304
+ | [awrelay](https://github.com/Aitherium/awrelay) | Portable agent messaging — findings, alerts, coordination | [docs](https://aitherium.github.io/awrelay/) |
305
+ | [awmail](https://github.com/Aitherium/awmail) | Give an agent an email address — send, and actually receive | — |
306
+ | [awfind](https://github.com/Aitherium/awfind) | A portable search client — query, results, ranking | — |
307
+ | [awbrowse](https://github.com/Aitherium/awbrowse) | A portable browser client — navigate, console, network, DOM, screenshot | — |
308
+ | [aitherkvcache](https://github.com/Aitherium/aitherkvcache) | Near-optimal KV cache quantization for LLM inference — sub-byte compression | [docs](https://aitherium.github.io/aitherkvcache/) |
309
+ | [AitherZero](https://github.com/Aitherium/AitherZero) | PowerShell 7+ automation framework — numbered, self-describing scripts | [docs](https://aitherium.github.io/AitherZero/) |
310
+ | [AitherConnect](https://github.com/Aitherium/AitherConnect) | Browser extension — federated AI search, page context, and the Living OS overlay | — |
311
+
312
+ <!-- aither-ecosystem:end -->
@@ -91,6 +91,19 @@ def _cmd_capture(args: argparse.Namespace) -> int:
91
91
  print(f"vcs: no semantic changes for {args.sha}")
92
92
  return 0
93
93
  print(f"vcs: op {op.op_id} recorded ({op.summary})")
94
+
95
+ # Record proof of verification if requested
96
+ if getattr(args, "prove", False):
97
+ try:
98
+ from awgit.outcomes import record_outcome
99
+ from awgit.prove import run_gates
100
+ data_root = Path(args.data_root) if args.data_root else None
101
+ gates = run_gates(op.file_paths)
102
+ outcome = record_outcome(args.sha, gates, data_root=data_root)
103
+ print(f"vcs: outcome {outcome.outcome_id} recorded ({outcome.verdict})")
104
+ except Exception as exc:
105
+ print(f"vcs: proof recording failed (non-fatal): {exc}", file=sys.stderr)
106
+
94
107
  return 0
95
108
 
96
109
 
@@ -232,10 +245,45 @@ def _cmd_status(args: argparse.Namespace) -> int:
232
245
  return 0
233
246
 
234
247
 
248
+ def _resolve_lease_args(registry, who, raw):
249
+ """Map release/heartbeat arguments to lease ids, accepting leased PATHS too.
250
+
251
+ `acquire` takes paths and `release` takes ids, so passing the same string to
252
+ both is the natural mistake -- and it used to be a silent one, because an
253
+ unmatched id simply released nothing and still printed success.
254
+
255
+ Returns (ids, unresolved). Anything that is neither one of this actor's
256
+ active lease ids nor one of its leased targets comes back in `unresolved`,
257
+ so the caller can fail rather than report a count of zero.
258
+ """
259
+ mine = [lz for lz in registry.active_leases() if lz.actor == who]
260
+ by_id = {lz.lease_id: lz.lease_id for lz in mine}
261
+ by_target = {}
262
+ for lz in mine:
263
+ # Last writer wins is fine: releasing any lease on that path is the
264
+ # intent, and a duplicate target for one actor is already a bug.
265
+ by_target[str(lz.target).replace("\\", "/").strip("/")] = lz.lease_id
266
+ ids, unresolved = [], []
267
+ for arg in raw or []:
268
+ if arg in by_id:
269
+ ids.append(arg)
270
+ continue
271
+ key = str(arg).replace("\\", "/").strip("/")
272
+ if key in by_target:
273
+ ids.append(by_target[key])
274
+ continue
275
+ unresolved.append(arg)
276
+ return ids, unresolved
277
+
278
+
235
279
  def _cmd_lease(args: argparse.Namespace) -> int:
236
280
  registry = LeaseRegistry()
237
281
  cmd = args.lease_cmd
238
282
  who = _actor(args)
283
+ if cmd == "contact":
284
+ return _cmd_lease_contact(args)
285
+ if cmd == "requests":
286
+ return _cmd_lease_requests(args)
239
287
  if cmd == "acquire":
240
288
  targets = list(args.targets or [])
241
289
  if getattr(args, "staged", False):
@@ -290,7 +338,19 @@ def _cmd_lease(args: argparse.Namespace) -> int:
290
338
  who, targets, ttl_sec=args.ttl, reason=args.reason
291
339
  )
292
340
  except LeaseConflictError as exc:
341
+ # Flush stdout FIRST. It is block-buffered when redirected to a file
342
+ # or a pipe and stderr is not, so this line otherwise lands
343
+ # INTERLEAVED in the middle of whatever stdout had buffered rather
344
+ # than at the end where anyone looks. Measured 2026-08-19:
345
+ # `lease acquire --staged --adopt` over 1776 files refused correctly
346
+ # on a real conflict, and the one line saying why was glued onto the
347
+ # middle of the adoption list at line 1657 of 1778 -- head and tail
348
+ # both missed it, and a correct refusal got reported as "awgit exits
349
+ # 1 and persists nothing". A diagnostic nobody can find is worse than
350
+ # none: it gets diagnosed as a different bug.
351
+ sys.stdout.flush()
293
352
  print(f"vcs: {exc}", file=sys.stderr)
353
+ sys.stderr.flush()
294
354
  return 1
295
355
  # A lease over an ALREADY-DIRTY file captures a baseline that contains work
296
356
  # which is not yours, and `stage-mine` computes (baseline -> worktree), so it
@@ -316,11 +376,28 @@ def _cmd_lease(args: argparse.Namespace) -> int:
316
376
  "`git diff --stat -- <path>` must match the size of YOUR edit.",
317
377
  file=sys.stderr)
318
378
  return 0
319
- if cmd == "heartbeat":
320
- print(f"vcs: heartbeat refreshed {registry.heartbeat(who, args.ids)} leases")
321
- return 0
322
- if cmd == "release":
323
- print(f"vcs: released {registry.release(who, args.ids)} leases")
379
+ if cmd in ("heartbeat", "release"):
380
+ # `release`/`heartbeat` take lease IDS. Passing a PATH -- the same string
381
+ # `acquire` takes, and the obvious guess -- matched no id, so the registry
382
+ # returned 0 and this printed "released 0 leases" and exited 0 while the
383
+ # lease sat there in `lease list`. A command that reports success for
384
+ # having done nothing is the silent-no-op class in
385
+ # .claude/rules/security-review-patterns.md #5, and it cost a session on
386
+ # 2026-08-16: the release "succeeded", the lease stayed held, and the next
387
+ # edit was blocked by the caller's own lease.
388
+ # Resolve path-shaped arguments against this actor's active leases, and
389
+ # refuse anything that resolves to nothing rather than reporting 0.
390
+ ids, unresolved = _resolve_lease_args(registry, who, args.ids)
391
+ if unresolved:
392
+ print("vcs: no active lease of yours matches: " + ", ".join(unresolved),
393
+ file=sys.stderr)
394
+ print("vcs: pass a lease id or a leased path (`awgit lease list`)",
395
+ file=sys.stderr)
396
+ return 1
397
+ if cmd == "heartbeat":
398
+ print(f"vcs: heartbeat refreshed {registry.heartbeat(who, ids)} leases")
399
+ else:
400
+ print(f"vcs: released {registry.release(who, ids)} leases")
324
401
  return 0
325
402
  if cmd == "list":
326
403
  for lz in sorted(registry.active_leases(), key=lambda x: x.target):
@@ -561,6 +638,95 @@ def _merge_hand_resolved(repo: Path, staged: List[str]) -> List[str]:
561
638
  return hand
562
639
 
563
640
 
641
+ def _requests_store():
642
+ """(LeaseRequests, registry) or (None, None) if the plane is unreadable."""
643
+ try:
644
+ from .lease_requests import LeaseRequests
645
+ from .leases import LeaseRegistry, vcs_data_root
646
+ reg = LeaseRegistry()
647
+ return LeaseRequests(vcs_data_root()), reg
648
+ except Exception as exc: # noqa: BLE001 - never fatal
649
+ print(f"vcs: lease-request store unavailable ({exc})", file=sys.stderr)
650
+ return None, None
651
+
652
+
653
+ def _relay_notify(to_actor: str, target: str, message: str) -> bool:
654
+ """Best-effort async ping. NEVER the only delivery path.
655
+
656
+ Returns False when it could not send, so the caller can say so rather than
657
+ implying the holder was reached. Relay is optional here on purpose: if the
658
+ only notification depended on a running service, this feature would be
659
+ unavailable in exactly the degraded conditions that produce lease pile-ups.
660
+ """
661
+ try:
662
+ from awrelay.client import RelayClient # type: ignore
663
+ except ImportError:
664
+ return False
665
+ try:
666
+ who = to_actor.split(":")[-1][:8]
667
+ RelayClient().send(
668
+ channel="lease-negotiation",
669
+ text=f"@{who} please release `{target}` — {message or 'another '
670
+ 'session is blocked on it'}")
671
+ return True
672
+ except Exception: # noqa: BLE001
673
+ return False
674
+
675
+
676
+ def _cmd_lease_contact(args) -> int:
677
+ """Ask whoever holds a path's lease to let it go."""
678
+ store, reg = _requests_store()
679
+ if store is None or reg is None:
680
+ return 2
681
+ me = _actor(args)
682
+ target = args.path
683
+
684
+ holder = None
685
+ for lz in reg.active_leases():
686
+ if lz.target == target:
687
+ holder = lz
688
+ break
689
+ if holder is None:
690
+ print(f"vcs: no active lease on {target!r} — nothing to ask for. "
691
+ f"If a commit was refused, re-run `awgit lease acquire`.")
692
+ return 0
693
+ if holder.actor == me:
694
+ print(f"vcs: {target!r} is held by YOU ({me}); "
695
+ f"`awgit lease release {holder.lease_id}` frees it.")
696
+ return 0
697
+
698
+ store.add(target, holder.actor, me, getattr(args, "message", "") or "")
699
+ relayed = _relay_notify(holder.actor, target,
700
+ getattr(args, "message", "") or "")
701
+ print(f"vcs: asked {holder.actor} to release {target!r} "
702
+ f"(expires {holder.expires_ts}).")
703
+ print("vcs: they see it on their next `awgit lease list`"
704
+ + (" and on relay #lease-negotiation." if relayed
705
+ else " (relay unavailable — the awgit path still delivers)."))
706
+ return 0
707
+
708
+
709
+ def _cmd_lease_requests(args) -> int:
710
+ """What am I blocking, and what am I waiting on?"""
711
+ store, _ = _requests_store()
712
+ if store is None:
713
+ return 2
714
+ me = _actor(args)
715
+ from .lease_requests import format_pending
716
+
717
+ incoming = store.for_actor(me)
718
+ outgoing = store.by_actor(me)
719
+ if incoming:
720
+ print(format_pending(incoming))
721
+ else:
722
+ print("vcs: nobody is blocked on leases you hold.")
723
+ if outgoing:
724
+ print(f"vcs: you have asked for {len(outgoing)} lease(s):")
725
+ for r in outgoing[:10]:
726
+ print(f"vcs: {r.get('target')} <- {str(r.get('to_actor'))[:24]}")
727
+ return 0
728
+
729
+
564
730
  def _cmd_lease_check(args: argparse.Namespace) -> int:
565
731
  if os.environ.get("VCS_LEASES_ENFORCE", "0") != "1":
566
732
  print("vcs: lease-check not enforced (VCS_LEASES_ENFORCE=0)")
@@ -1477,6 +1643,11 @@ def build_parser() -> argparse.ArgumentParser:
1477
1643
  default=None,
1478
1644
  help="vcs store directory (default: ~/.aither/awgit/data, or $VCS_DATA_ROOT)",
1479
1645
  )
1646
+ p_capture.add_argument(
1647
+ "--prove",
1648
+ action="store_true",
1649
+ help="run gates and record the outcome after capture",
1650
+ )
1480
1651
 
1481
1652
  p_diff = sub.add_parser("diff", help="node-level diff between two shas")
1482
1653
  p_diff.add_argument("a", help="base sha")
@@ -1555,6 +1726,17 @@ def build_parser() -> argparse.ArgumentParser:
1555
1726
  p_ll = lsub.add_parser("list", help="list active leases")
1556
1727
  p_ll.add_argument("--json", action="store_true", dest="as_json")
1557
1728
  lsub.add_parser("sweep", help="sweep expired leases")
1729
+ p_lc = lsub.add_parser(
1730
+ "contact",
1731
+ help="ask whoever holds a path's lease to release it -- the missing "
1732
+ "half of 'talk to them or wait'")
1733
+ p_lc.add_argument("path")
1734
+ p_lc.add_argument("-m", "--message", default="", help="why you need it")
1735
+ p_lc.add_argument("--actor", default=None)
1736
+ p_lq = lsub.add_parser(
1737
+ "requests",
1738
+ help="who is blocked on YOUR leases, and what you are waiting on")
1739
+ p_lq.add_argument("--actor", default=None)
1558
1740
 
1559
1741
  p_lc = sub.add_parser("lease-check", help="pre-commit lease gate")
1560
1742
  p_lc.add_argument("--actor", default=None)
@@ -144,6 +144,123 @@ def check(actor: str, cwd: Optional[Path] = None) -> Verdict:
144
144
  return Verdict(True, basis="no other actor holds a lease in this worktree")
145
145
 
146
146
 
147
+ #: The branch a PUBLISHED artifact is supposed to come from. Overridable, because
148
+ #: a product with its own release branch is a real thing -- but it has a default,
149
+ #: because "which branch should this have been built from" is not a question the
150
+ #: person mid-deploy should have to answer.
151
+ DEFAULT_DEPLOY_REF = "origin/develop"
152
+
153
+
154
+ @dataclass
155
+ class ShipVerdict:
156
+ """Is this working tree fit to build a PUBLISHED artifact from?
157
+
158
+ Separate from `Verdict` on purpose: that one asks whether rewriting history
159
+ here would destroy a peer's work, this one asks whether shipping from here
160
+ would publish something old. Same tree, different danger.
161
+ """
162
+
163
+ ok: bool
164
+ behind: int = 0
165
+ ahead: int = 0
166
+ ref: str = DEFAULT_DEPLOY_REF
167
+ head: str = ""
168
+ reasons: List[str] = field(default_factory=list)
169
+ fix: List[str] = field(default_factory=list)
170
+ #: True when the question could not be ANSWERED (no such ref, not a repo).
171
+ #: Callers must treat this as a refusal, never as a pass -- an unanswerable
172
+ #: staleness check is exactly the state that let a stale build ship.
173
+ unknown: bool = False
174
+
175
+ def report(self) -> str:
176
+ head, *rest = self.reasons or ["unfit to publish from"]
177
+ lines = [f"awgit: refusing to publish — {head}"]
178
+ lines += [f"awgit: {r}" for r in rest]
179
+ lines += [f"awgit: {f}" for f in self.fix]
180
+ return "\n".join(lines)
181
+
182
+
183
+ def behind_deploy_ref(
184
+ cwd: Optional[Path] = None, ref: str = DEFAULT_DEPLOY_REF
185
+ ) -> ShipVerdict:
186
+ """How far the current checkout is behind the branch releases come from.
187
+
188
+ 🚨 WHY THIS EXISTS, measured 2026-08-20. Two customer-facing sites were
189
+ published from a feature branch **587 commits behind the deploy branch**. The build succeeded, every gate
190
+ passed, the export was byte-valid, and the sites came up looking right. What
191
+ shipped was a desktop carrying a bug that had been FIXED on develop earlier
192
+ that same day (the Aeon window could not be dragged, because its body was not
193
+ a containing block, so app content painted over the only drag handle).
194
+
195
+ Nothing in the pipeline could see it. A build does not know what it is missing;
196
+ `git status` is clean on a stale branch; and the artifact is *correct*, just
197
+ old. The only signal was the owner recognising a bug he had already had fixed.
198
+
199
+ Two hours of that session were also spent re-deriving fixes that already
200
+ existed on develop -- the same route exclusion, the same malformed blog post.
201
+ Staleness does not only ship old code, it burns the time spent rebuilding what
202
+ someone already built.
203
+
204
+ Answers from the LOCAL ref: this must be usable in a publish path that may have
205
+ no network, and a fetch that silently fails would turn "up to date" into a lie.
206
+ `stale_ref_age_days` reports how old the local ref is so a caller can insist on
207
+ a fetch when it matters, rather than this function pretending to know.
208
+ """
209
+ repo = repo_root(cwd)
210
+ if repo is None:
211
+ return ShipVerdict(
212
+ ok=False, unknown=True, ref=ref,
213
+ reasons=["not inside a git repository, so staleness cannot be judged"],
214
+ fix=["run this from the repo, or pass an explicit --allow-stale reason"],
215
+ )
216
+
217
+ head = _git(repo, "rev-parse", "--short", "HEAD").strip()
218
+ # `rev-list --count A..B` counts commits in B not in A.
219
+ raw = _git(repo, "rev-list", "--left-right", "--count", f"HEAD...{ref}").strip()
220
+ parts = raw.split()
221
+ if len(parts) != 2 or not all(x.isdigit() for x in parts):
222
+ return ShipVerdict(
223
+ ok=False, unknown=True, ref=ref, head=head,
224
+ reasons=[f"could not compare HEAD against {ref} (is it fetched?)"],
225
+ fix=[f"git fetch origin && retry, or pass --allow-stale with a reason"],
226
+ )
227
+ ahead, behind = int(parts[0]), int(parts[1])
228
+ if behind == 0:
229
+ return ShipVerdict(ok=True, behind=0, ahead=ahead, ref=ref, head=head)
230
+
231
+ return ShipVerdict(
232
+ ok=False, behind=behind, ahead=ahead, ref=ref, head=head,
233
+ reasons=[
234
+ f"HEAD ({head}) is {behind} commit(s) BEHIND {ref}",
235
+ "a build from here succeeds and ships code that was already fixed —"
236
+ " there is no other signal, because the artifact is correct, just old",
237
+ ],
238
+ fix=[
239
+ f"fix: git fetch origin && git merge {ref} (or rebuild from {ref})",
240
+ "override only with a stated reason: --allow-stale '<why>'",
241
+ ],
242
+ )
243
+
244
+
245
+ def stale_ref_age_days(cwd: Optional[Path] = None,
246
+ ref: str = DEFAULT_DEPLOY_REF) -> Optional[float]:
247
+ """Age in days of the LOCAL copy of `ref`, or None if it cannot be read.
248
+
249
+ `behind_deploy_ref` compares against whatever was last fetched. A ref fetched
250
+ a week ago can report "0 behind" and be badly wrong, so the age is the second
251
+ half of the answer and callers should surface it.
252
+ """
253
+ repo = repo_root(cwd)
254
+ if repo is None:
255
+ return None
256
+ out = _git(repo, "log", "-1", "--format=%ct", ref).strip()
257
+ if not out.isdigit():
258
+ return None
259
+ import time
260
+
261
+ return max(0.0, (time.time() - int(out)) / 86400.0)
262
+
263
+
147
264
  def require(actor: str, cwd: Optional[Path] = None) -> Optional[int]:
148
265
  """``None`` when a rewrite may proceed, else an exit code after reporting.
149
266