gitgrip 1.5.0__py3-none-any.whl

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 (80) hide show
  1. gitgrip-1.5.0.dist-info/METADATA +13 -0
  2. gitgrip-1.5.0.dist-info/RECORD +80 -0
  3. gitgrip-1.5.0.dist-info/WHEEL +5 -0
  4. gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
  5. gitgrip-1.5.0.dist-info/top_level.txt +2 -0
  6. gr2/__init__.py +0 -0
  7. gr2/overlay/__init__.py +6 -0
  8. gr2/overlay/activate.py +196 -0
  9. gr2/overlay/agent_manifest.py +138 -0
  10. gr2/overlay/cli.py +181 -0
  11. gr2/overlay/cross_repo.py +124 -0
  12. gr2/overlay/drivers.py +113 -0
  13. gr2/overlay/introspection.py +155 -0
  14. gr2/overlay/language_drivers.py +115 -0
  15. gr2/overlay/objects.py +412 -0
  16. gr2/overlay/perf.py +251 -0
  17. gr2/overlay/refs.py +36 -0
  18. gr2/overlay/trust.py +150 -0
  19. gr2/overlay/types.py +69 -0
  20. gr2/overlay/units.py +313 -0
  21. gr2/overlay/workspace_spec.py +59 -0
  22. gr2/prototypes/__init__.py +0 -0
  23. gr2/prototypes/cache_materialization_probe.py +190 -0
  24. gr2/prototypes/concurrent_event_stress.py +199 -0
  25. gr2/prototypes/concurrent_lease_stress.py +240 -0
  26. gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
  27. gr2/prototypes/contribution_protocol.py +665 -0
  28. gr2/prototypes/cross_mode_lane_stress.py +986 -0
  29. gr2/prototypes/jsonl_store.py +158 -0
  30. gr2/prototypes/lane_workspace_prototype.py +2088 -0
  31. gr2/prototypes/layout_model_probe.py +139 -0
  32. gr2/prototypes/propagation_daemon.py +546 -0
  33. gr2/prototypes/propagation_state_machine.py +1478 -0
  34. gr2/prototypes/python_exec_playground.py +194 -0
  35. gr2/prototypes/python_hook_runtime_playground.py +240 -0
  36. gr2/prototypes/python_migration_playground.py +144 -0
  37. gr2/prototypes/python_review_checkout_playground.py +242 -0
  38. gr2/prototypes/python_spec_apply_playground.py +282 -0
  39. gr2/prototypes/real_git_lane_materialization.py +248 -0
  40. gr2/prototypes/real_git_playground.py +334 -0
  41. gr2/prototypes/recall_lane_history.py +274 -0
  42. gr2/prototypes/repo_maintenance_prototype.py +659 -0
  43. gr2/prototypes/repo_transport_probe.py +147 -0
  44. gr2/python_cli/__init__.py +2 -0
  45. gr2/python_cli/__main__.py +6 -0
  46. gr2/python_cli/add.py +51 -0
  47. gr2/python_cli/app.py +2516 -0
  48. gr2/python_cli/branch.py +67 -0
  49. gr2/python_cli/channel_bridge.py +131 -0
  50. gr2/python_cli/clone_exec.py +1019 -0
  51. gr2/python_cli/commit.py +199 -0
  52. gr2/python_cli/config.py +291 -0
  53. gr2/python_cli/env_exec.py +419 -0
  54. gr2/python_cli/events.py +529 -0
  55. gr2/python_cli/execops.py +372 -0
  56. gr2/python_cli/failures.py +98 -0
  57. gr2/python_cli/file_exec.py +256 -0
  58. gr2/python_cli/gitops.py +226 -0
  59. gr2/python_cli/grip.py +1337 -0
  60. gr2/python_cli/grip_cli.py +493 -0
  61. gr2/python_cli/hooks.py +450 -0
  62. gr2/python_cli/launch_exec.py +786 -0
  63. gr2/python_cli/merge_verification.py +274 -0
  64. gr2/python_cli/migration.py +985 -0
  65. gr2/python_cli/open_gr_review.py +699 -0
  66. gr2/python_cli/platform.py +441 -0
  67. gr2/python_cli/pr.py +487 -0
  68. gr2/python_cli/project_review.py +314 -0
  69. gr2/python_cli/prune.py +365 -0
  70. gr2/python_cli/push.py +172 -0
  71. gr2/python_cli/review.py +462 -0
  72. gr2/python_cli/review_ephemeral.py +143 -0
  73. gr2/python_cli/review_run.py +621 -0
  74. gr2/python_cli/spec_apply.py +1285 -0
  75. gr2/python_cli/staging_cleanup.py +205 -0
  76. gr2/python_cli/syncops.py +920 -0
  77. gr2/python_cli/target.py +100 -0
  78. gr2/python_cli/workspace_snapshot.py +105 -0
  79. gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
  80. gr2_overlay/__init__.py +37 -0
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitgrip
3
+ Version: 1.5.0
4
+ Summary: gr2 overlay substrate: config-overlay capture, composition, and materialization
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: typer>=0.9
7
+ Requires-Dist: tomli_w>=1.0
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: toml>=0.10
10
+ Requires-Dist: jsonschema>=4.18
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: ruff>=0.4; extra == "dev"
@@ -0,0 +1,80 @@
1
+ gr2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ gr2/overlay/__init__.py,sha256=4_s69T3rS3qAoegOyniFNSeEcJn8nq6UjK9HVSWVKEQ,186
3
+ gr2/overlay/activate.py,sha256=5ji6Mmr50bA3K1Ghn-VyPlp8doRldl2WJQLl4oyJvw0,6387
4
+ gr2/overlay/agent_manifest.py,sha256=nsTAqj1112NlYe1rxW_x62BI37gksXEAUxXv-NEix-c,4198
5
+ gr2/overlay/cli.py,sha256=VqhhBhO-4wxEbE0Y5YNKm0i2TZV8VTYux5bLyDWEGR4,6723
6
+ gr2/overlay/cross_repo.py,sha256=is6OD_dgQYn5kjR6ng0gJlDXtPBZ6e_zuHiowv6F6ow,3755
7
+ gr2/overlay/drivers.py,sha256=VeWVobi5bJOt_E4JGFMI8mkfqwkCafCcn0KlNjJ4IvU,3599
8
+ gr2/overlay/introspection.py,sha256=4mnPtOpZBWppxgZZ1sHGFD8DKuvmYSXZxiBeB1aeX0U,4644
9
+ gr2/overlay/language_drivers.py,sha256=VKNWc9WlEm_yBVKevEoZP7UJSR50NLXmejRD0vPq71A,3595
10
+ gr2/overlay/objects.py,sha256=d8z8a5SWyj1IADfk9nGDUvA-UVO6L3KMbNGHv3e-7Ag,12945
11
+ gr2/overlay/perf.py,sha256=2Cl-b29u88DZfyxtMmdfijC8_tSzzED2rCLgIFQfZlk,7717
12
+ gr2/overlay/refs.py,sha256=APGuXI97_qohcMKOKDS0t0I5tXK6_lf9oJFsIU9Q2YA,908
13
+ gr2/overlay/trust.py,sha256=h5tmLtfrnKUKNFpLa_RgLZgYOKIh_PPEgMalyRMRLaU,4049
14
+ gr2/overlay/types.py,sha256=gnrFWz5CU_zyftYXQXtuTSwdvpycME5qrKPfmNuwy5k,1535
15
+ gr2/overlay/units.py,sha256=e9DWUdlEYsIVyRcXQEWtc8Iefn4WEuKY4kztk5ky4Lc,10199
16
+ gr2/overlay/workspace_spec.py,sha256=LMh6GLActDR0BmyJ-BPfuCrUaq6Y1EDlXwgMtf0Ds0k,1658
17
+ gr2/prototypes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ gr2/prototypes/cache_materialization_probe.py,sha256=L2P-IddHtpHcHiXdwCQP2NekMtrV8VYBHCRrtW2aDzs,6134
19
+ gr2/prototypes/concurrent_event_stress.py,sha256=4fWLyzAhJQ_Vz3kSl8HRH3YXCMF9X2G5IOG4PIq_5SY,7170
20
+ gr2/prototypes/concurrent_lease_stress.py,sha256=KNB_3c6xna0Wc15m9fUE-TJrpXi8lT-4bWBPdHjUmCU,6748
21
+ gr2/prototypes/concurrent_workspace_cap_stress.py,sha256=ghKjM7xFCywL0lF54hgav_lhS1py_mo3j6Bw0dyQTow,6757
22
+ gr2/prototypes/contribution_protocol.py,sha256=wBezx6YkHCzZfnv4p1WZrTYHYIoCNayHtcyq1nEg_Es,26158
23
+ gr2/prototypes/cross_mode_lane_stress.py,sha256=7fMb-ewOhcZLb41Gw_oDjf2T7gqx32gNJ5c91gQUuew,31421
24
+ gr2/prototypes/jsonl_store.py,sha256=8SRcB7mCZt9rrslKzaZ-ok3kRWkPgb_7634-hCZ2VC0,5950
25
+ gr2/prototypes/lane_workspace_prototype.py,sha256=XhkW1c0ltWfp5Wa4Gn8B2H8s32Mr_MQK_bXya1WIfvg,85184
26
+ gr2/prototypes/layout_model_probe.py,sha256=dQDJd6TYlPzzkQmkdOg9Jvs2djEtH-xedLxHbPKPYxk,4978
27
+ gr2/prototypes/propagation_daemon.py,sha256=6qWcZ7yELPISWmNWLr5vVW2fCIl8hC6J1h-MC2JSnN0,21899
28
+ gr2/prototypes/propagation_state_machine.py,sha256=kArpari1-KA9mhq8gOo0EgDiNstTaqD6mJttxq7Uevg,57143
29
+ gr2/prototypes/python_exec_playground.py,sha256=tBLHszG9hDHXhfWuERCNHkEdUAKjVoZQoMGaxAY4Po4,5944
30
+ gr2/prototypes/python_hook_runtime_playground.py,sha256=9pWyFZLIyvuPTQ_fLSS1V-IpArUMEyPGtizUC8wNn7A,8370
31
+ gr2/prototypes/python_migration_playground.py,sha256=HqGSB7A5HEEGXC4lgIpsbfCYaiojNELWvBTovA6OH28,5729
32
+ gr2/prototypes/python_review_checkout_playground.py,sha256=kcZYTJdXdTONp1w9qPoLcGr7GqKbHd_6gD9SbOKauj8,8612
33
+ gr2/prototypes/python_spec_apply_playground.py,sha256=GvbNh53DuvLrXk5WZSIJXs7qqlAdLiHyXXXN3SkDPac,11303
34
+ gr2/prototypes/real_git_lane_materialization.py,sha256=FeDsHv7pBFLz9KlTbG8jp2Sqfe1svqid1_wdvGHwq0U,7602
35
+ gr2/prototypes/real_git_playground.py,sha256=JqgbC842FtcfrTAeDesLhk2MAJpmVWbs4BcXhmB2sfk,12983
36
+ gr2/prototypes/recall_lane_history.py,sha256=qSZWnRyr6t3t32jSSdZq2TjzJqzQzU-KTE1vXQdhYT4,8131
37
+ gr2/prototypes/repo_maintenance_prototype.py,sha256=SbQttDUjffHCZP-cB0D63wNd3IUP5Tcg9yYDUvtjwkY,21080
38
+ gr2/prototypes/repo_transport_probe.py,sha256=k6NMEbpljYfT_lUDZz7uAvdGElYooZCQ52x_bbFegSE,4945
39
+ gr2/python_cli/__init__.py,sha256=DthM50YXkXW4sWkVKwKbfcKHPhsitLf6xBWD-nD6uqs,37
40
+ gr2/python_cli/__main__.py,sha256=aJblLyFv6F_O2kpDDxZY369huhCrIxLXD1TDFVOXTWc,61
41
+ gr2/python_cli/add.py,sha256=_TkEPln9BHhGeITXOejlNwCcRgn75uUBseOa8rfSqFo,1771
42
+ gr2/python_cli/app.py,sha256=KPjjNgY_X79MrNKRKpW9AXIER3ev4i4QLtYdv0249P0,108465
43
+ gr2/python_cli/branch.py,sha256=yf2qgBUB7S68qv8muqTLb5CFoLx1BxWGL5YpnDHP-PM,2408
44
+ gr2/python_cli/channel_bridge.py,sha256=w3wJRzwbDqpC8BwSyTXdYb3GdmFXCUBVaYyMQ_ryQIU,4879
45
+ gr2/python_cli/clone_exec.py,sha256=l0oYhHsl7WGcSmwhSZscFr8JkhWtCEbHj--6XA5Zbeo,46792
46
+ gr2/python_cli/commit.py,sha256=Ar6Q-yGpMMg_0h4-WV9n79qghM6MalXjcF-ibhwYxK8,7288
47
+ gr2/python_cli/config.py,sha256=QSIAT524X9w5E2o0L_NAxuovIvJtBIkn5BntNbM4kGI,9421
48
+ gr2/python_cli/env_exec.py,sha256=pNeVuegeZ5CxoMsbe3PbnbisCn3b7087wFmV6R2kALA,16569
49
+ gr2/python_cli/events.py,sha256=vrmeZ6C92t0r1Z8jgTMX9r-d_ADr5VWGmnm-tRJiSrg,19990
50
+ gr2/python_cli/execops.py,sha256=VS8l65ETU1blqPsf_wLxcbOtHgNWL6M_r8sxTFe7JQM,12315
51
+ gr2/python_cli/failures.py,sha256=XOru8scLDvbhU_tL4D-4e2y4IR1G5jmikJGyOF1Yd5M,2794
52
+ gr2/python_cli/file_exec.py,sha256=BVRyvX6higMwZTzRP5VReACjFfqW6mib_of7EA0e6Yk,11132
53
+ gr2/python_cli/gitops.py,sha256=oAqK0NLRARLpO75sjEEgnWWcDz_W_3cwW4Sr1qI9XaU,8138
54
+ gr2/python_cli/grip.py,sha256=FHR_dH2mquFvLWJdae6aFRYG8sO0OqhIyGiiDaRkH78,61608
55
+ gr2/python_cli/grip_cli.py,sha256=oS-6G2wzPlE_fzcqWT4wJdWrt9gZU2UJpeulKwusQiU,17250
56
+ gr2/python_cli/hooks.py,sha256=eugnbG7JwRDB7pjUgMWTdRLMyZevLqj1eHFE8c483H0,14792
57
+ gr2/python_cli/launch_exec.py,sha256=bumUwzNEWbGDMvHuUQSHqFkDkqgzkLhVh51hVNGPzrI,31481
58
+ gr2/python_cli/merge_verification.py,sha256=C_-lALciljGk1Q6pS252Xy3iyAmAW2uqFK7CyWph9es,8903
59
+ gr2/python_cli/migration.py,sha256=fujBxGRna4DBl3V-MvPn4auWdo5clPn6_e9ab_9tolM,46071
60
+ gr2/python_cli/open_gr_review.py,sha256=iAfXpg1triota1lZDTDrPBtF8SDd2gJBNVucuHD42Vg,34406
61
+ gr2/python_cli/platform.py,sha256=6O3N3zohSd7DawpHcHaUykOLoCoPmvZk2dLiEQCq8Xo,13389
62
+ gr2/python_cli/pr.py,sha256=yYD71Ysrq4YkttRyGLpaYhwtgdmolCSzksRa7meBxgE,15186
63
+ gr2/python_cli/project_review.py,sha256=j0y_YfbxLv1xAG6ropE_pY1E50hHmACHtpRJNvKVLTs,16301
64
+ gr2/python_cli/prune.py,sha256=-v7UvxWPiCQD293OAQ4qZbti5tx8RCvlURtSSf1V00A,14641
65
+ gr2/python_cli/push.py,sha256=3rMjEzjfwpnsDszInVL8I3cYOHUkyRUr67YdvTrM4Vw,6183
66
+ gr2/python_cli/review.py,sha256=3JU8_coFKy6zov1kxcmYHwi0SFb2QFl209xckaDMM2A,22669
67
+ gr2/python_cli/review_ephemeral.py,sha256=oxusB79olOpg2iJE6FW9aCmrT7IHT7xSHRDgDTCbVNU,6349
68
+ gr2/python_cli/review_run.py,sha256=-9glNT70uuq7a5KySOOQkRmLTu1bxQc-J8hXcqeiMpE,28232
69
+ gr2/python_cli/spec_apply.py,sha256=o1s1sx5TN5dttYBh7PK8byvdFtqVvZY4oGBM6yiN3Fo,56255
70
+ gr2/python_cli/staging_cleanup.py,sha256=4BZ0E9U_08yCG0k8SaIHgA9oBv-wdP4ktQT_XYvSlIY,9103
71
+ gr2/python_cli/syncops.py,sha256=ebgEJ_oy5wAeARlVHK6AOaB_goLf8DlhYQMg9DcbJvg,34328
72
+ gr2/python_cli/target.py,sha256=tLVtwQ87oll8EVGHvXexkNgHQxbXMljchif60BO5D9A,3455
73
+ gr2/python_cli/workspace_snapshot.py,sha256=0wU-xlWZnRVp-Nqt2c7kajKNbTtXxY8RZ44zKjBT7QU,4761
74
+ gr2/schemas/gr2-materialization-plan-v1.schema.json,sha256=pQYVAbpmUddDLYfVfxyFkC5d7AdvhgpH-qKZ9fWQIxw,4415
75
+ gr2_overlay/__init__.py,sha256=Z6e6GR77z4jnqquv2WJ9Vu2S5yewe-po2hL4B8W3NSs,1521
76
+ gitgrip-1.5.0.dist-info/METADATA,sha256=2J_ab564_Pw6CuZDrpwGkj2OtiNPOSxh-WETlnXUpBA,407
77
+ gitgrip-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
78
+ gitgrip-1.5.0.dist-info/entry_points.txt,sha256=ZNqbxRz8sChINyvZFQfOTMhKQOm3_MXXJatZDAUqsBM,48
79
+ gitgrip-1.5.0.dist-info/top_level.txt,sha256=5S-GGt6uSGhg8VcYVkjObRfE1NqWDr-2DDGpR1H5ajo,16
80
+ gitgrip-1.5.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gr2 = gr2.python_cli.app:main
@@ -0,0 +1,2 @@
1
+ gr2
2
+ gr2_overlay
gr2/__init__.py ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ """gr2 overlay substrate: config-overlay capture, composition, and materialization.
2
+
3
+ M1 scope: Tier A only (config files), eager materialization, trust-gated.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -0,0 +1,196 @@
1
+ """Overlay activate/deactivate: eager materialization and reversible teardown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ from gr2.overlay.objects import apply_overlay_object
11
+ from gr2.overlay.trust import (
12
+ OverlayTrustError,
13
+ authorize_overlay_driver,
14
+ load_workspace_allowlist,
15
+ trust_config_path,
16
+ )
17
+ from gr2.overlay.types import OverlayRef
18
+
19
+ GRIP_DIR = ".grip"
20
+ STACK_FILE = "overlay-stack.json"
21
+ MANAGED_FILE = "overlay-managed.json"
22
+
23
+
24
+ @dataclass
25
+ class ActivationResult:
26
+ status: str
27
+ completed: list[str] = field(default_factory=list)
28
+
29
+
30
+ @dataclass
31
+ class DeactivationResult:
32
+ completed: list[str] = field(default_factory=list)
33
+
34
+
35
+ class OverlayActivationError(Exception):
36
+ def __init__(self, message: str, error_code: str) -> None:
37
+ super().__init__(message)
38
+ self.error_code = error_code
39
+
40
+
41
+ def activate_overlay(
42
+ workspace_root: Path,
43
+ overlay_store: Path,
44
+ overlay_ref: OverlayRef,
45
+ overlay_source_kind: str,
46
+ overlay_source_value: str | None,
47
+ overlay_signer: str | None,
48
+ ) -> ActivationResult:
49
+ _check_trust(
50
+ workspace_root, overlay_ref, overlay_source_kind, overlay_source_value, overlay_signer
51
+ )
52
+
53
+ grip_dir = workspace_root / GRIP_DIR
54
+ base_state = grip_dir / "overlay-base-state.toml"
55
+ if base_state.exists():
56
+ raise OverlayActivationError(
57
+ "Base has advanced since overlay was captured", error_code="base_advanced"
58
+ )
59
+
60
+ force_conflict = grip_dir / "force-conflict.toml"
61
+ if force_conflict.exists():
62
+ raise OverlayActivationError(
63
+ "Composition conflict detected", error_code="composition_conflict"
64
+ )
65
+
66
+ _remove_managed_files_for(workspace_root, overlay_ref)
67
+
68
+ apply_overlay_object(overlay_store, overlay_ref, workspace_root)
69
+
70
+ managed_files = _read_overlay_file_list(overlay_store, overlay_ref)
71
+ _write_state(workspace_root, overlay_ref, managed_files)
72
+
73
+ return ActivationResult(status="ok", completed=["overlay.activated"])
74
+
75
+
76
+ def deactivate_overlay(
77
+ workspace_root: Path,
78
+ overlay_ref: OverlayRef,
79
+ ) -> DeactivationResult:
80
+ grip_dir = workspace_root / GRIP_DIR
81
+ managed = _load_managed_files(workspace_root)
82
+
83
+ file_list = managed.get(overlay_ref.ref_path, [])
84
+ for rel_path in file_list:
85
+ target = workspace_root / rel_path
86
+ if target.exists():
87
+ target.unlink()
88
+ parent = target.parent
89
+ while parent != workspace_root and parent.exists() and not any(parent.iterdir()):
90
+ parent.rmdir()
91
+ parent = parent.parent
92
+
93
+ managed.pop(overlay_ref.ref_path, None)
94
+ stack = read_active_overlay_stack(workspace_root)
95
+ stack = [ref for ref in stack if ref != overlay_ref.ref_path]
96
+
97
+ grip_dir.mkdir(parents=True, exist_ok=True)
98
+ if stack:
99
+ (grip_dir / STACK_FILE).write_text(json.dumps(stack))
100
+ (grip_dir / MANAGED_FILE).write_text(json.dumps(managed))
101
+ else:
102
+ (grip_dir / STACK_FILE).unlink(missing_ok=True)
103
+ (grip_dir / MANAGED_FILE).unlink(missing_ok=True)
104
+ if not any(grip_dir.iterdir()):
105
+ grip_dir.rmdir()
106
+
107
+ return DeactivationResult(completed=["overlay.deactivated"])
108
+
109
+
110
+ def read_active_overlay_stack(workspace_root: Path) -> list[str]:
111
+ stack_file = workspace_root / GRIP_DIR / STACK_FILE
112
+ if not stack_file.exists():
113
+ return []
114
+ return json.loads(stack_file.read_text())
115
+
116
+
117
+ def _check_trust(
118
+ workspace_root: Path,
119
+ overlay_ref: OverlayRef,
120
+ overlay_source_kind: str,
121
+ overlay_source_value: str | None,
122
+ overlay_signer: str | None,
123
+ ) -> None:
124
+ config = trust_config_path(workspace_root)
125
+ if not config.exists():
126
+ return
127
+
128
+ allowlist = load_workspace_allowlist(workspace_root)
129
+ try:
130
+ authorize_overlay_driver(
131
+ driver_name="overlay-deep",
132
+ overlay_ref=overlay_ref,
133
+ overlay_source_kind=overlay_source_kind,
134
+ overlay_source_value=overlay_source_value,
135
+ overlay_signer=overlay_signer,
136
+ allowlist=allowlist,
137
+ )
138
+ except OverlayTrustError as e:
139
+ raise OverlayActivationError(str(e), error_code=e.error_code) from e
140
+
141
+
142
+ def _read_overlay_file_list(overlay_store: Path, overlay_ref: OverlayRef) -> list[str]:
143
+ tag_oid = _git_output(overlay_store, "rev-parse", overlay_ref.ref_path)
144
+ structured_tree_oid = _git_output(overlay_store, "rev-parse", f"{tag_oid}^{{tree}}")
145
+ wt_line = _git_output(overlay_store, "ls-tree", structured_tree_oid, "working_tree_tree")
146
+ working_tree_oid = wt_line.split()[2]
147
+ ls_output = _git_output(overlay_store, "ls-tree", "-r", "--name-only", working_tree_oid)
148
+ if not ls_output:
149
+ return []
150
+ return sorted(ls_output.splitlines())
151
+
152
+
153
+ def _write_state(workspace_root: Path, overlay_ref: OverlayRef, managed_files: list[str]) -> None:
154
+ grip_dir = workspace_root / GRIP_DIR
155
+ grip_dir.mkdir(parents=True, exist_ok=True)
156
+
157
+ stack_file = grip_dir / STACK_FILE
158
+ stack = read_active_overlay_stack(workspace_root)
159
+ if overlay_ref.ref_path not in stack:
160
+ stack.append(overlay_ref.ref_path)
161
+ stack_file.write_text(json.dumps(stack))
162
+
163
+ managed_file = grip_dir / MANAGED_FILE
164
+ managed = _load_managed_files(workspace_root)
165
+ managed[overlay_ref.ref_path] = managed_files
166
+ managed_file.write_text(json.dumps(managed))
167
+
168
+
169
+ def _remove_managed_files_for(workspace_root: Path, overlay_ref: OverlayRef) -> None:
170
+ managed = _load_managed_files(workspace_root)
171
+ file_list = managed.get(overlay_ref.ref_path, [])
172
+ for rel_path in file_list:
173
+ target = workspace_root / rel_path
174
+ if target.exists():
175
+ target.unlink()
176
+ parent = target.parent
177
+ while parent != workspace_root and parent.exists() and not any(parent.iterdir()):
178
+ parent.rmdir()
179
+ parent = parent.parent
180
+
181
+
182
+ def _load_managed_files(workspace_root: Path) -> dict[str, list[str]]:
183
+ managed_file = workspace_root / GRIP_DIR / MANAGED_FILE
184
+ if not managed_file.exists():
185
+ return {}
186
+ return json.loads(managed_file.read_text())
187
+
188
+
189
+ def _git_output(git_dir: Path, *args: str) -> str:
190
+ result = subprocess.run(
191
+ ["git", f"--git-dir={git_dir}", *args],
192
+ check=True,
193
+ capture_output=True,
194
+ text=True,
195
+ )
196
+ return result.stdout.strip()
@@ -0,0 +1,138 @@
1
+ """Agent manifest reader: YAML frontmatter from COMPOSE.md with base-wins precedence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+
13
+ class AgentManifestValidationError(Exception):
14
+ pass
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class AgentManifest:
19
+ description: str
20
+ language: str
21
+ build: str
22
+ test: str
23
+ lint: str | None = None
24
+ format: str | None = None
25
+ source_path: Path | None = None
26
+ source_kind: str = "overlay"
27
+ repo_name: str | None = None
28
+
29
+
30
+ _FRONTMATTER_RE = re.compile(r"\A---\s*\n(.*?)\n---", re.DOTALL)
31
+
32
+ _STRING_FIELDS = ("description", "language", "build", "test", "lint", "format")
33
+
34
+
35
+ def read_effective_agent_manifest(
36
+ *,
37
+ repo_root: Path,
38
+ overlay_root: Path,
39
+ ) -> AgentManifest:
40
+ base_compose = repo_root / "COMPOSE.md"
41
+ overlay_compose = overlay_root / "COMPOSE.md"
42
+
43
+ if base_compose.exists():
44
+ raw = _extract_agent_block(base_compose)
45
+ if raw is not None:
46
+ _validate(raw, base_compose)
47
+ return _build_manifest(raw, source_path=base_compose, source_kind="base")
48
+
49
+ if overlay_compose.exists():
50
+ raw = _extract_agent_block(overlay_compose)
51
+ if raw is not None:
52
+ _validate(raw, overlay_compose)
53
+ return _build_manifest(raw, source_path=overlay_compose, source_kind="overlay")
54
+
55
+ raise AgentManifestValidationError("No valid COMPOSE.md with agent frontmatter found")
56
+
57
+
58
+ def read_workspace_repo_agent_manifest(
59
+ *,
60
+ workspace_root: Path,
61
+ repo_name: str,
62
+ overlays_root: Path,
63
+ ) -> AgentManifest:
64
+ repo_root = _resolve_repo_root(workspace_root, repo_name)
65
+ overlay_root = overlays_root / repo_name
66
+
67
+ manifest = read_effective_agent_manifest(
68
+ repo_root=repo_root,
69
+ overlay_root=overlay_root,
70
+ )
71
+
72
+ return AgentManifest(
73
+ description=manifest.description,
74
+ language=manifest.language,
75
+ build=manifest.build,
76
+ test=manifest.test,
77
+ lint=manifest.lint,
78
+ format=manifest.format,
79
+ source_path=manifest.source_path,
80
+ source_kind=manifest.source_kind,
81
+ repo_name=repo_name,
82
+ )
83
+
84
+
85
+ def _resolve_repo_root(workspace_root: Path, repo_name: str) -> Path:
86
+ """Resolve repo root from gripspace.yml manifest, falling back to reference/<name>."""
87
+ manifest_path = workspace_root / ".gitgrip" / "spaces" / "main" / "gripspace.yml"
88
+ if manifest_path.exists():
89
+ manifest = yaml.safe_load(manifest_path.read_text()) or {}
90
+ repos = manifest.get("repos", {}) or {}
91
+ repo_entry = repos.get(repo_name, {}) or {}
92
+ repo_path = str(repo_entry.get("path", "")).strip()
93
+ if repo_path:
94
+ normalized = repo_path[2:] if repo_path.startswith("./") else repo_path
95
+ resolved = workspace_root / normalized
96
+ if resolved.is_dir():
97
+ return resolved
98
+ return workspace_root / "reference" / repo_name
99
+
100
+
101
+ def _extract_agent_block(compose_path: Path) -> dict[str, Any] | None:
102
+ text = compose_path.read_text()
103
+ m = _FRONTMATTER_RE.match(text)
104
+ if m is None:
105
+ return None
106
+ frontmatter = yaml.safe_load(m.group(1))
107
+ if not isinstance(frontmatter, dict):
108
+ return None
109
+ return frontmatter.get("agent")
110
+
111
+
112
+ def _validate(raw: Any, source: Path) -> None:
113
+ if not isinstance(raw, dict):
114
+ raise AgentManifestValidationError(f"agent block in {source} is not a mapping")
115
+ for field in _STRING_FIELDS:
116
+ value = raw.get(field)
117
+ if value is not None and not isinstance(value, str):
118
+ raise AgentManifestValidationError(
119
+ f"Field '{field}' in {source} must be a string, got {type(value).__name__}"
120
+ )
121
+
122
+
123
+ def _build_manifest(
124
+ raw: dict[str, Any],
125
+ *,
126
+ source_path: Path,
127
+ source_kind: str,
128
+ ) -> AgentManifest:
129
+ return AgentManifest(
130
+ description=raw.get("description", ""),
131
+ language=raw.get("language", ""),
132
+ build=raw.get("build", ""),
133
+ test=raw.get("test", ""),
134
+ lint=raw.get("lint"),
135
+ format=raw.get("format"),
136
+ source_path=source_path,
137
+ source_kind=source_kind,
138
+ )
gr2/overlay/cli.py ADDED
@@ -0,0 +1,181 @@
1
+ """CLI plumbing for `gr overlay` subcommands.
2
+
3
+ Wires typer commands to gr2.overlay module functions.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+ import typer
12
+
13
+ from gr2.overlay import introspection, workspace_spec
14
+ from gr2.overlay.activate import activate_overlay, deactivate_overlay
15
+ from gr2.overlay.types import OverlayRef
16
+
17
+ overlay_app = typer.Typer(
18
+ help="Config overlay capture, composition, and materialization (Tier A).",
19
+ )
20
+
21
+
22
+ def _default_overlay_store(workspace_root: Path) -> Path:
23
+ return workspace_root / ".grip" / "overlay-store.git"
24
+
25
+
26
+ def _emit(result: str | dict | list, json_output: bool) -> None:
27
+ if json_output:
28
+ if isinstance(result, str):
29
+ typer.echo(result)
30
+ else:
31
+ typer.echo(json.dumps(result, indent=2))
32
+ else:
33
+ typer.echo(result)
34
+
35
+
36
+ @overlay_app.command("activate")
37
+ def overlay_activate_cmd(
38
+ workspace_root: Path,
39
+ ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
40
+ source_kind: str = typer.Option("local", help="Overlay source kind (local, git, registry)"),
41
+ source_value: str | None = typer.Option(None, help="Overlay source value (path or URL)"),
42
+ signer: str | None = typer.Option(None, help="Expected overlay signer"),
43
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
44
+ ) -> None:
45
+ """Activate an overlay, eagerly materializing its files into the workspace."""
46
+ overlay_ref = OverlayRef.parse(ref)
47
+ overlay_store = _default_overlay_store(workspace_root)
48
+ result = activate_overlay(
49
+ workspace_root=workspace_root,
50
+ overlay_store=overlay_store,
51
+ overlay_ref=overlay_ref,
52
+ overlay_source_kind=source_kind,
53
+ overlay_source_value=source_value,
54
+ overlay_signer=signer,
55
+ )
56
+ if json_output:
57
+ typer.echo(json.dumps({"status": result.status, "completed": result.completed}))
58
+ else:
59
+ typer.echo(f"Activated {ref}: {result.status}")
60
+
61
+
62
+ @overlay_app.command("deactivate")
63
+ def overlay_deactivate_cmd(
64
+ workspace_root: Path,
65
+ ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
66
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
67
+ ) -> None:
68
+ """Deactivate an overlay and remove its materialized files."""
69
+ overlay_ref = OverlayRef.parse(ref)
70
+ result = deactivate_overlay(
71
+ workspace_root=workspace_root,
72
+ overlay_ref=overlay_ref,
73
+ )
74
+ if json_output:
75
+ typer.echo(json.dumps({"completed": result.completed}))
76
+ else:
77
+ typer.echo(f"Deactivated {ref}")
78
+
79
+
80
+ @overlay_app.command("diff")
81
+ def overlay_diff_cmd(
82
+ workspace_root: Path,
83
+ ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
84
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
85
+ ) -> None:
86
+ """Show the diff an overlay would produce if activated."""
87
+ overlay_ref = OverlayRef.parse(ref)
88
+ overlay_store = _default_overlay_store(workspace_root)
89
+ file_list = introspection._read_overlay_file_list(overlay_store, overlay_ref)
90
+ if json_output:
91
+ typer.echo(json.dumps({"ref": ref, "files": file_list}))
92
+ else:
93
+ typer.echo(f"Files that would change for {ref}:")
94
+ for f in file_list:
95
+ typer.echo(f" {f}")
96
+
97
+
98
+ @overlay_app.command("list")
99
+ def overlay_list_cmd(
100
+ workspace_root: Path,
101
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
102
+ ) -> None:
103
+ """List all declared overlays from workspace spec."""
104
+ entries = workspace_spec.load_overlay_spec(workspace_root)
105
+ if json_output:
106
+ overlay_data = [
107
+ {
108
+ "name": e.name,
109
+ "path": e.path,
110
+ "applies_to": e.applies_to,
111
+ "priority": e.priority,
112
+ }
113
+ for e in entries
114
+ ]
115
+ typer.echo(json.dumps({"overlays": overlay_data}, indent=2))
116
+ else:
117
+ if not entries:
118
+ typer.echo("No overlays declared.")
119
+ else:
120
+ typer.echo("Declared overlays:")
121
+ for e in entries:
122
+ typer.echo(f" {e.name} (priority={e.priority}, path={e.path})")
123
+
124
+
125
+ @overlay_app.command("stack")
126
+ def overlay_stack_cmd(
127
+ workspace_root: Path,
128
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
129
+ ) -> None:
130
+ """Show the current overlay activation stack with priority ordering."""
131
+ overlay_store = _default_overlay_store(workspace_root)
132
+ result = introspection.overlay_stack(workspace_root, overlay_store, json_output)
133
+ _emit(result, json_output)
134
+
135
+
136
+ @overlay_app.command("status")
137
+ def overlay_status_cmd(
138
+ workspace_root: Path,
139
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
140
+ ) -> None:
141
+ """Show overlay materialization status: which overlays are active, stale, or conflicting."""
142
+ overlay_store = _default_overlay_store(workspace_root)
143
+ result = introspection.overlay_status(workspace_root, overlay_store, json_output)
144
+ _emit(result, json_output)
145
+
146
+
147
+ @overlay_app.command("trace")
148
+ def overlay_trace_cmd(
149
+ workspace_root: Path,
150
+ file_path: str = typer.Argument(..., help="File path to trace overlay provenance for"),
151
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
152
+ ) -> None:
153
+ """Trace which overlay(s) contributed to a specific file's current state."""
154
+ overlay_store = _default_overlay_store(workspace_root)
155
+ result = introspection.overlay_trace(workspace_root, overlay_store, file_path, json_output)
156
+ _emit(result, json_output)
157
+
158
+
159
+ @overlay_app.command("why")
160
+ def overlay_why_cmd(
161
+ workspace_root: Path,
162
+ key: str = typer.Argument(..., help="Config key or file path to explain"),
163
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
164
+ ) -> None:
165
+ """Explain why a config key has its current value (which overlay, base, or merge)."""
166
+ overlay_store = _default_overlay_store(workspace_root)
167
+ result = introspection.overlay_why(workspace_root, overlay_store, key, json_output)
168
+ _emit(result, json_output)
169
+
170
+
171
+ @overlay_app.command("impact")
172
+ def overlay_impact_cmd(
173
+ workspace_root: Path,
174
+ ref: str = typer.Argument(..., help="Overlay ref: <author>/<name>"),
175
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON"),
176
+ ) -> None:
177
+ """Show what files and keys would change if this overlay were activated or deactivated."""
178
+ overlay_ref = OverlayRef.parse(ref)
179
+ overlay_store = _default_overlay_store(workspace_root)
180
+ result = introspection.overlay_impact(overlay_store, overlay_ref, json_output)
181
+ _emit(result, json_output)