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,242 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ import tomllib
9
+ from pathlib import Path
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+
14
+
15
+ def run(args: list[str], *, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
16
+ env = os.environ.copy()
17
+ env["PYTHONPATH"] = str(ROOT)
18
+ proc = subprocess.run(args, cwd=cwd, env=env, text=True, capture_output=True)
19
+ if check and proc.returncode != 0:
20
+ raise SystemExit(f"command failed: {args}\nstdout={proc.stdout}\nstderr={proc.stderr}")
21
+ return proc
22
+
23
+
24
+ def pygr2(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
25
+ return run(["python3", "-m", "gr2.python_cli", *args], cwd=cwd, check=check)
26
+
27
+
28
+ def git(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
29
+ return run(["git", *args], cwd=cwd, check=check)
30
+
31
+
32
+ def seed_remote_with_review_branch(remote: Path, branch_name: str) -> None:
33
+ git("init", "--bare", str(remote))
34
+ with tempfile.TemporaryDirectory(prefix="gr2-review-seed-") as td:
35
+ seed = Path(td) / "seed"
36
+ git("clone", str(remote), str(seed))
37
+ git("config", "user.name", "Atlas", cwd=seed)
38
+ git("config", "user.email", "atlas@example.com", cwd=seed)
39
+ (seed / "README.md").write_text("# demo\n")
40
+ git("add", "README.md", cwd=seed)
41
+ git("commit", "-m", "seed", cwd=seed)
42
+ git("push", "origin", "HEAD:main", cwd=seed)
43
+ git("checkout", "-b", branch_name, cwd=seed)
44
+ (seed / "REVIEW.txt").write_text("review branch\n")
45
+ git("add", "REVIEW.txt", cwd=seed)
46
+ git("commit", "-m", "review branch", cwd=seed)
47
+ git("push", "origin", f"HEAD:{branch_name}", cwd=seed)
48
+ git("--git-dir", str(remote), "symbolic-ref", "HEAD", "refs/heads/main")
49
+
50
+
51
+ def advance_review_branch(remote: Path, branch_name: str) -> None:
52
+ with tempfile.TemporaryDirectory(prefix="gr2-review-advance-") as td:
53
+ seed = Path(td) / "seed"
54
+ git("clone", str(remote), str(seed))
55
+ git("config", "user.name", "Atlas", cwd=seed)
56
+ git("config", "user.email", "atlas@example.com", cwd=seed)
57
+ git("checkout", branch_name, cwd=seed)
58
+ (seed / "REVIEW_2.txt").write_text("updated review branch\n")
59
+ git("add", "REVIEW_2.txt", cwd=seed)
60
+ git("commit", "-m", "review branch update", cwd=seed)
61
+ git("push", "origin", f"HEAD:{branch_name}", cwd=seed)
62
+
63
+
64
+ def remove_lane_checkout(shared_repo_root: Path, lane_repo_root: Path) -> None:
65
+ git("-C", str(shared_repo_root), "worktree", "remove", "--force", str(lane_repo_root))
66
+
67
+
68
+ def write_spec(workspace_root: Path, remote: Path) -> None:
69
+ (workspace_root / ".grip").mkdir(parents=True, exist_ok=True)
70
+ (workspace_root / ".grip" / "workspace_spec.toml").write_text(
71
+ "\n".join(
72
+ [
73
+ 'workspace_name = "review-playground"',
74
+ "",
75
+ "[[repos]]",
76
+ 'name = "demo"',
77
+ 'path = "repos/demo"',
78
+ f'url = "{remote.as_posix()}"',
79
+ "",
80
+ "[[units]]",
81
+ 'name = "atlas"',
82
+ 'path = "agents/atlas/home"',
83
+ 'repos = ["demo"]',
84
+ "",
85
+ ]
86
+ )
87
+ )
88
+
89
+
90
+ def scenario_review_checkout_and_enter() -> dict[str, object]:
91
+ with tempfile.TemporaryDirectory(prefix="gr2-review-playground-") as td:
92
+ workspace_root = Path(td)
93
+ remote = workspace_root / "demo-remote.git"
94
+ review_branch = "review/demo-42"
95
+ seed_remote_with_review_branch(remote, review_branch)
96
+ write_spec(workspace_root, remote)
97
+ pygr2("apply", str(workspace_root), "--yes")
98
+
99
+ payload = json.loads(
100
+ pygr2(
101
+ "review",
102
+ "checkout-pr",
103
+ str(workspace_root),
104
+ "atlas",
105
+ "demo",
106
+ "42",
107
+ "--branch",
108
+ review_branch,
109
+ "--enter",
110
+ "--actor",
111
+ "agent:atlas",
112
+ "--json",
113
+ ).stdout
114
+ )
115
+
116
+ lane_repo_root = Path(payload["lane_repo_root"])
117
+ lane_file = workspace_root / "agents" / "atlas" / "lanes" / "review-42" / "lane.toml"
118
+ current_file = workspace_root / ".grip" / "state" / "current_lane" / "atlas.json"
119
+ current = json.loads(current_file.read_text())
120
+ lane_doc = tomllib.loads(lane_file.read_text())
121
+ branch = git("branch", "--show-current", cwd=lane_repo_root).stdout.strip()
122
+
123
+ return {
124
+ "name": "review-checkout-and-enter",
125
+ "holds": payload["lane_name"] == "review-42"
126
+ and payload["branch"] == review_branch
127
+ and payload["entered"] is True
128
+ and lane_repo_root.joinpath(".git").exists()
129
+ and lane_repo_root.joinpath("REVIEW.txt").exists()
130
+ and branch == review_branch
131
+ and current["current"]["lane_name"] == "review-42"
132
+ and lane_doc["lane_type"] == "review"
133
+ and lane_doc["pr_associations"][0]["ref"] == "demo#42",
134
+ "payload": payload,
135
+ }
136
+
137
+
138
+ def scenario_missing_shared_repo_blocks() -> dict[str, object]:
139
+ with tempfile.TemporaryDirectory(prefix="gr2-review-missing-shared-") as td:
140
+ workspace_root = Path(td)
141
+ remote = workspace_root / "demo-remote.git"
142
+ review_branch = "review/demo-42"
143
+ seed_remote_with_review_branch(remote, review_branch)
144
+ write_spec(workspace_root, remote)
145
+ proc = pygr2(
146
+ "review",
147
+ "checkout-pr",
148
+ str(workspace_root),
149
+ "atlas",
150
+ "demo",
151
+ "42",
152
+ "--branch",
153
+ review_branch,
154
+ "--json",
155
+ check=False,
156
+ )
157
+ return {
158
+ "name": "missing-shared-repo-blocks",
159
+ "holds": proc.returncode != 0 and "shared repo missing for review checkout" in (proc.stderr + proc.stdout),
160
+ }
161
+
162
+
163
+ def scenario_existing_local_branch_refetches() -> dict[str, object]:
164
+ with tempfile.TemporaryDirectory(prefix="gr2-review-refetch-") as td:
165
+ workspace_root = Path(td)
166
+ remote = workspace_root / "demo-remote.git"
167
+ review_branch = "review/demo-42"
168
+ seed_remote_with_review_branch(remote, review_branch)
169
+ write_spec(workspace_root, remote)
170
+ pygr2("apply", str(workspace_root), "--yes")
171
+
172
+ pygr2(
173
+ "review",
174
+ "checkout-pr",
175
+ str(workspace_root),
176
+ "atlas",
177
+ "demo",
178
+ "42",
179
+ "--lane",
180
+ "review-42-old",
181
+ "--branch",
182
+ review_branch,
183
+ "--json",
184
+ )
185
+
186
+ shared_repo_root = workspace_root / "repos" / "demo"
187
+ old_lane_repo_root = workspace_root / "agents" / "atlas" / "lanes" / "review-42-old" / "repos" / "demo"
188
+ remove_lane_checkout(shared_repo_root, old_lane_repo_root)
189
+
190
+ advance_review_branch(remote, review_branch)
191
+
192
+ payload = json.loads(
193
+ pygr2(
194
+ "review",
195
+ "checkout-pr",
196
+ str(workspace_root),
197
+ "atlas",
198
+ "demo",
199
+ "42",
200
+ "--lane",
201
+ "review-42-new",
202
+ "--branch",
203
+ review_branch,
204
+ "--json",
205
+ ).stdout
206
+ )
207
+
208
+ lane_repo_root = Path(payload["lane_repo_root"])
209
+ return {
210
+ "name": "existing-local-branch-refetches",
211
+ "holds": lane_repo_root.joinpath("REVIEW_2.txt").exists(),
212
+ "payload": payload,
213
+ }
214
+
215
+
216
+ SCENARIOS = [
217
+ scenario_review_checkout_and_enter,
218
+ scenario_missing_shared_repo_blocks,
219
+ scenario_existing_local_branch_refetches,
220
+ ]
221
+
222
+
223
+ def main() -> int:
224
+ parser = argparse.ArgumentParser(description="Playground verification for Python gr2 review checkout-pr.")
225
+ parser.add_argument("--json", action="store_true")
226
+ args = parser.parse_args()
227
+
228
+ results = [scenario() for scenario in SCENARIOS]
229
+ verdict = "holds" if all(item["holds"] for item in results) else "gaps"
230
+ payload = {"verdict": verdict, "results": results}
231
+
232
+ if args.json:
233
+ print(json.dumps(payload, indent=2))
234
+ else:
235
+ print(f"python review checkout verdict: {verdict}")
236
+ for item in results:
237
+ print(f"- {item['name']}: {'holds' if item['holds'] else 'gaps'}")
238
+ return 0 if verdict == "holds" else 1
239
+
240
+
241
+ if __name__ == "__main__":
242
+ raise SystemExit(main())
@@ -0,0 +1,282 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+
14
+
15
+ def run(args: list[str], *, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
16
+ env = os.environ.copy()
17
+ env["PYTHONPATH"] = str(ROOT)
18
+ proc = subprocess.run(args, cwd=cwd, env=env, text=True, capture_output=True)
19
+ if check and proc.returncode != 0:
20
+ raise SystemExit(f"command failed: {args}\nstdout={proc.stdout}\nstderr={proc.stderr}")
21
+ return proc
22
+
23
+
24
+ def pygr2(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
25
+ return run(["python3", "-m", "gr2.python_cli", *args], cwd=cwd, check=check)
26
+
27
+
28
+ def git(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
29
+ return run(["git", *args], cwd=cwd, check=check)
30
+
31
+
32
+ def write_workspace_spec(workspace_root: Path, remote: Path) -> None:
33
+ grip_dir = workspace_root / ".grip"
34
+ grip_dir.mkdir(parents=True, exist_ok=True)
35
+ (grip_dir / "workspace_spec.toml").write_text(
36
+ "\n".join(
37
+ [
38
+ 'workspace_name = "playground"',
39
+ "",
40
+ "[[repos]]",
41
+ 'name = "demo"',
42
+ 'path = "repos/demo"',
43
+ f'url = "{remote.as_posix()}"',
44
+ "",
45
+ "[[units]]",
46
+ 'name = "atlas"',
47
+ 'path = "agents/atlas/home"',
48
+ 'repos = ["demo"]',
49
+ "",
50
+ "[[units]]",
51
+ 'name = "apollo"',
52
+ 'path = "agents/apollo/home"',
53
+ 'repos = ["demo"]',
54
+ "",
55
+ ]
56
+ )
57
+ )
58
+
59
+
60
+ def seed_bare_remote(remote: Path, *, with_hooks: bool = False) -> None:
61
+ git("init", "--bare", str(remote))
62
+ with tempfile.TemporaryDirectory(prefix="gr2-playground-seed-") as td:
63
+ seed = Path(td) / "seed"
64
+ git("clone", str(remote), str(seed))
65
+ git("config", "user.name", "Atlas", cwd=seed)
66
+ git("config", "user.email", "atlas@example.com", cwd=seed)
67
+ (seed / "README.md").write_text("# demo\n")
68
+ if with_hooks:
69
+ hooks_dir = seed / ".gr2"
70
+ hooks_dir.mkdir(parents=True, exist_ok=True)
71
+ (hooks_dir / "hooks.toml").write_text(
72
+ "\n".join(
73
+ [
74
+ "[[files.link]]",
75
+ 'src = "{workspace_root}/config/claude.md"',
76
+ 'dest = "{repo_root}/CLAUDE.md"',
77
+ 'if_exists = "overwrite"',
78
+ "",
79
+ "[[lifecycle.on_materialize]]",
80
+ 'name = "materialize-marker"',
81
+ "command = \"python3 -c \\\"from pathlib import Path; Path('MATERIALIZED').write_text('ok\\\\n')\\\"\"",
82
+ 'cwd = "{repo_root}"',
83
+ 'when = "first_materialize"',
84
+ 'on_failure = "block"',
85
+ "",
86
+ ]
87
+ )
88
+ )
89
+ git("add", "README.md", cwd=seed)
90
+ if with_hooks:
91
+ git("add", ".gr2/hooks.toml", cwd=seed)
92
+ git("commit", "-m", "seed", cwd=seed)
93
+ git("push", "origin", "HEAD:main", cwd=seed)
94
+
95
+
96
+ def create_demo_lane_state(workspace_root: Path) -> None:
97
+ lane_root = workspace_root / "agents" / "atlas" / "lanes" / "feat-auth"
98
+ (lane_root / "repos" / "demo").mkdir(parents=True, exist_ok=True)
99
+ current_dir = workspace_root / ".grip" / "state" / "lanes" / "atlas"
100
+ current_dir.mkdir(parents=True, exist_ok=True)
101
+ (current_dir / "current.json").write_text(
102
+ json.dumps(
103
+ {
104
+ "current": {
105
+ "lane_name": "feat-auth",
106
+ "entered_at": "2026-04-14T12:00:00Z",
107
+ }
108
+ },
109
+ indent=2,
110
+ )
111
+ )
112
+
113
+
114
+ def scenario_missing_spec() -> dict[str, object]:
115
+ with tempfile.TemporaryDirectory(prefix="gr2-spec-missing-") as td:
116
+ workspace_root = Path(td)
117
+ proc = pygr2("plan", str(workspace_root), check=False)
118
+ return {
119
+ "name": "missing-spec",
120
+ "holds": proc.returncode != 0 and "workspace spec not found" in (proc.stderr + proc.stdout),
121
+ "returncode": proc.returncode,
122
+ }
123
+
124
+
125
+ def scenario_path_conflict() -> dict[str, object]:
126
+ with tempfile.TemporaryDirectory(prefix="gr2-path-conflict-") as td:
127
+ workspace_root = Path(td)
128
+ remote = workspace_root / "demo-remote.git"
129
+ seed_bare_remote(remote)
130
+ write_workspace_spec(workspace_root, remote)
131
+ (workspace_root / "repos" / "demo").mkdir(parents=True, exist_ok=True)
132
+ proc = pygr2("spec", "validate", str(workspace_root), "--json", check=False)
133
+ payload = json.loads(proc.stdout)
134
+ return {
135
+ "name": "path-conflict",
136
+ "holds": proc.returncode == 1 and any(item["code"] == "repo_path_conflict" for item in payload["issues"]),
137
+ "issues": payload["issues"],
138
+ }
139
+
140
+
141
+ def scenario_fresh_workspace() -> dict[str, object]:
142
+ with tempfile.TemporaryDirectory(prefix="gr2-fresh-workspace-") as td:
143
+ workspace_root = Path(td)
144
+ remote = workspace_root / "demo-remote.git"
145
+ (workspace_root / "config").mkdir(parents=True, exist_ok=True)
146
+ (workspace_root / "config" / "claude.md").write_text("shared claude\n")
147
+ seed_bare_remote(remote, with_hooks=True)
148
+ write_workspace_spec(workspace_root, remote)
149
+
150
+ plan_before = json.loads(pygr2("plan", str(workspace_root), "--json").stdout)
151
+ apply_payload = json.loads(pygr2("apply", str(workspace_root), "--yes", "--json").stdout)
152
+ plan_after = json.loads(pygr2("plan", str(workspace_root), "--json").stdout)
153
+ repo_root = workspace_root / "repos" / "demo"
154
+ cache_root = workspace_root / ".grip" / "cache" / "repos" / "demo.git"
155
+ alternates = repo_root / ".git" / "objects" / "info" / "alternates"
156
+
157
+ expected_kinds = [
158
+ "seed_repo_cache",
159
+ "clone_repo",
160
+ "create_unit_root",
161
+ "write_unit_metadata",
162
+ "create_unit_root",
163
+ "write_unit_metadata",
164
+ ]
165
+
166
+ return {
167
+ "name": "fresh-workspace",
168
+ "holds": [item["kind"] for item in plan_before] == expected_kinds
169
+ and apply_payload["operation_count"] == 6
170
+ and plan_after == []
171
+ and repo_root.joinpath(".git").exists()
172
+ and (workspace_root / "agents" / "atlas" / "home" / "unit.toml").exists()
173
+ and (workspace_root / "agents" / "apollo" / "home" / "unit.toml").exists()
174
+ and cache_root.exists()
175
+ and alternates.exists()
176
+ and repo_root.joinpath("CLAUDE.md").exists()
177
+ and repo_root.joinpath("MATERIALIZED").exists(),
178
+ "plan_before": plan_before,
179
+ "apply_payload": apply_payload,
180
+ "cache_root_exists": cache_root.exists(),
181
+ "alternates_exists": alternates.exists(),
182
+ "claude_link_exists": repo_root.joinpath("CLAUDE.md").exists(),
183
+ "materialize_marker_exists": repo_root.joinpath("MATERIALIZED").exists(),
184
+ }
185
+
186
+
187
+ def scenario_dirty_shared_repo_preserved() -> dict[str, object]:
188
+ with tempfile.TemporaryDirectory(prefix="gr2-dirty-shared-") as td:
189
+ workspace_root = Path(td)
190
+ remote = workspace_root / "demo-remote.git"
191
+ seed_bare_remote(remote)
192
+ write_workspace_spec(workspace_root, remote)
193
+ pygr2("apply", str(workspace_root), "--yes")
194
+ repo_root = workspace_root / "repos" / "demo"
195
+ (repo_root / "LOCAL.txt").write_text("dirty\n")
196
+ plan_payload = json.loads(pygr2("plan", str(workspace_root), "--json").stdout)
197
+ apply_payload = json.loads(pygr2("apply", str(workspace_root), "--json").stdout)
198
+ preserved = (repo_root / "LOCAL.txt").exists()
199
+ return {
200
+ "name": "dirty-shared-repo-preserved",
201
+ "holds": plan_payload == [] and apply_payload["operation_count"] == 0 and preserved,
202
+ "plan_payload": plan_payload,
203
+ "apply_payload": apply_payload,
204
+ }
205
+
206
+
207
+ def scenario_lane_state_untouched() -> dict[str, object]:
208
+ with tempfile.TemporaryDirectory(prefix="gr2-lane-state-") as td:
209
+ workspace_root = Path(td)
210
+ remote = workspace_root / "demo-remote.git"
211
+ seed_bare_remote(remote)
212
+ write_workspace_spec(workspace_root, remote)
213
+ create_demo_lane_state(workspace_root)
214
+ before = (workspace_root / ".grip" / "state" / "lanes" / "atlas" / "current.json").read_text()
215
+ pygr2("apply", str(workspace_root), "--yes")
216
+ after = (workspace_root / ".grip" / "state" / "lanes" / "atlas" / "current.json").read_text()
217
+ lane_checkout_still_absent = not (workspace_root / "agents" / "atlas" / "lanes" / "feat-auth" / "repos" / "demo" / ".git").exists()
218
+ return {
219
+ "name": "lane-state-untouched",
220
+ "holds": before == after and lane_checkout_still_absent,
221
+ }
222
+
223
+
224
+ def scenario_invalid_repo_hooks() -> dict[str, object]:
225
+ with tempfile.TemporaryDirectory(prefix="gr2-invalid-hooks-") as td:
226
+ workspace_root = Path(td)
227
+ remote = workspace_root / "demo-remote.git"
228
+ seed_bare_remote(remote)
229
+ write_workspace_spec(workspace_root, remote)
230
+ pygr2("apply", str(workspace_root), "--yes")
231
+ hooks_dir = workspace_root / "repos" / "demo" / ".gr2"
232
+ hooks_dir.mkdir(parents=True, exist_ok=True)
233
+ (hooks_dir / "hooks.toml").write_text(
234
+ "\n".join(
235
+ [
236
+ "[[lifecycle.on_enter]]",
237
+ 'name = "broken"',
238
+ 'command = "true"',
239
+ 'when = "not-a-real-when"',
240
+ "",
241
+ ]
242
+ )
243
+ )
244
+ proc = pygr2("spec", "validate", str(workspace_root), "--json", check=False)
245
+ payload = json.loads(proc.stdout)
246
+ return {
247
+ "name": "invalid-repo-hooks",
248
+ "holds": proc.returncode == 1 and any(item["code"] == "invalid_repo_hooks" for item in payload["issues"]),
249
+ "issues": payload["issues"],
250
+ }
251
+
252
+
253
+ SCENARIOS = [
254
+ scenario_missing_spec,
255
+ scenario_path_conflict,
256
+ scenario_fresh_workspace,
257
+ scenario_dirty_shared_repo_preserved,
258
+ scenario_lane_state_untouched,
259
+ scenario_invalid_repo_hooks,
260
+ ]
261
+
262
+
263
+ def main() -> int:
264
+ parser = argparse.ArgumentParser(description="Playground verification for Python gr2 spec/plan/apply.")
265
+ parser.add_argument("--json", action="store_true")
266
+ args = parser.parse_args()
267
+
268
+ results = [scenario() for scenario in SCENARIOS]
269
+ verdict = "holds" if all(item["holds"] for item in results) else "gaps"
270
+ payload = {"verdict": verdict, "results": results}
271
+
272
+ if args.json:
273
+ print(json.dumps(payload, indent=2))
274
+ else:
275
+ print(f"python spec/apply playground verdict: {verdict}")
276
+ for item in results:
277
+ print(f"- {item['name']}: {'holds' if item['holds'] else 'gaps'}")
278
+ return 0 if verdict == "holds" else 1
279
+
280
+
281
+ if __name__ == "__main__":
282
+ raise SystemExit(main())