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,194 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+
13
+
14
+ def run(args: list[str], *, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
15
+ env = os.environ.copy()
16
+ env["PYTHONPATH"] = str(ROOT)
17
+ proc = subprocess.run(args, cwd=cwd, env=env, text=True, capture_output=True)
18
+ if check and proc.returncode != 0:
19
+ raise SystemExit(f"command failed: {args}\nstdout={proc.stdout}\nstderr={proc.stderr}")
20
+ return proc
21
+
22
+
23
+ def pygr2(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
24
+ return run(["python3", "-m", "gr2.python_cli", *args], cwd=cwd, check=check)
25
+
26
+
27
+ def git(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
28
+ return run(["git", *args], cwd=cwd, check=check)
29
+
30
+
31
+ def seed_remote(remote: Path) -> None:
32
+ git("init", "--bare", str(remote))
33
+ with tempfile.TemporaryDirectory(prefix="gr2-exec-seed-") as td:
34
+ seed = Path(td) / "seed"
35
+ git("clone", str(remote), str(seed))
36
+ git("config", "user.name", "Atlas", cwd=seed)
37
+ git("config", "user.email", "atlas@example.com", cwd=seed)
38
+ (seed / "README.md").write_text("# demo\n")
39
+ git("add", "README.md", cwd=seed)
40
+ git("commit", "-m", "seed", cwd=seed)
41
+ git("push", "origin", "HEAD:main", cwd=seed)
42
+ git("--git-dir", str(remote), "symbolic-ref", "HEAD", "refs/heads/main")
43
+
44
+
45
+ def write_spec(workspace_root: Path, remote: Path) -> None:
46
+ grip_dir = workspace_root / ".grip"
47
+ grip_dir.mkdir(parents=True, exist_ok=True)
48
+ (grip_dir / "workspace_spec.toml").write_text(
49
+ "\n".join(
50
+ [
51
+ 'workspace_name = "exec-playground"',
52
+ "",
53
+ "[[repos]]",
54
+ 'name = "demo"',
55
+ 'path = "repos/demo"',
56
+ f'url = "{remote.as_posix()}"',
57
+ "",
58
+ "[[units]]",
59
+ 'name = "atlas"',
60
+ 'path = "agents/atlas/home"',
61
+ 'repos = ["demo"]',
62
+ "",
63
+ ]
64
+ )
65
+ )
66
+
67
+
68
+ def base_workspace() -> Path:
69
+ workspace_root = Path(tempfile.mkdtemp(prefix="gr2-exec-playground-"))
70
+ remote = workspace_root / "demo-remote.git"
71
+ seed_remote(remote)
72
+ write_spec(workspace_root, remote)
73
+ pygr2("apply", str(workspace_root), "--yes")
74
+ pygr2(
75
+ "lane",
76
+ "create",
77
+ str(workspace_root),
78
+ "atlas",
79
+ "feat-auth",
80
+ "--repos",
81
+ "demo",
82
+ "--branch",
83
+ "feat/auth",
84
+ )
85
+ pygr2("lane", "enter", str(workspace_root), "atlas", "feat-auth", "--actor", "agent:atlas")
86
+ return workspace_root
87
+
88
+
89
+ def scenario_exec_status_ready() -> dict[str, object]:
90
+ workspace_root = base_workspace()
91
+ payload = json.loads(pygr2("exec", "status", str(workspace_root), "atlas", "--json").stdout)
92
+ row = payload["rows"][0]
93
+ return {
94
+ "name": "exec-status-ready",
95
+ "holds": payload["status"] == "ready"
96
+ and payload["lane"] == "feat-auth"
97
+ and row["repo"] == "demo"
98
+ and row["exists"] is True
99
+ and row["cwd"].endswith("/agents/atlas/lanes/feat-auth/repos/demo"),
100
+ "payload": payload,
101
+ }
102
+
103
+
104
+ def scenario_exec_run_success() -> dict[str, object]:
105
+ workspace_root = base_workspace()
106
+ payload = json.loads(
107
+ pygr2(
108
+ "exec",
109
+ "run",
110
+ str(workspace_root),
111
+ "atlas",
112
+ "python3",
113
+ "-c",
114
+ "from pathlib import Path; Path('EXEC_OK').write_text('ok\\n')",
115
+ "--actor",
116
+ "agent:atlas",
117
+ "--json",
118
+ ).stdout
119
+ )
120
+ repo_root = workspace_root / "agents" / "atlas" / "lanes" / "feat-auth" / "repos" / "demo"
121
+ leases_path = workspace_root / "agents" / "atlas" / "lanes" / "feat-auth" / "leases.json"
122
+ leases = json.loads(leases_path.read_text()) if leases_path.exists() else []
123
+ return {
124
+ "name": "exec-run-success",
125
+ "holds": payload["status"] == "success"
126
+ and payload["results"][0]["status"] == "ok"
127
+ and repo_root.joinpath("EXEC_OK").exists()
128
+ and leases == [],
129
+ "payload": payload,
130
+ }
131
+
132
+
133
+ def scenario_exec_blocked_by_edit() -> dict[str, object]:
134
+ workspace_root = base_workspace()
135
+ pygr2(
136
+ "lane",
137
+ "lease",
138
+ "acquire",
139
+ str(workspace_root),
140
+ "atlas",
141
+ "feat-auth",
142
+ "--actor",
143
+ "human:layne",
144
+ "--mode",
145
+ "edit",
146
+ )
147
+ proc = pygr2(
148
+ "exec",
149
+ "run",
150
+ str(workspace_root),
151
+ "atlas",
152
+ "pwd",
153
+ "--actor",
154
+ "agent:atlas",
155
+ "--json",
156
+ check=False,
157
+ )
158
+ payload = json.loads(proc.stdout)
159
+ return {
160
+ "name": "exec-blocked-by-edit",
161
+ "holds": proc.returncode == 1
162
+ and payload["status"] == "blocked"
163
+ and payload["reason"] == "conflicting-active-lease",
164
+ "payload": payload,
165
+ }
166
+
167
+
168
+ SCENARIOS = [
169
+ scenario_exec_status_ready,
170
+ scenario_exec_run_success,
171
+ scenario_exec_blocked_by_edit,
172
+ ]
173
+
174
+
175
+ def main() -> int:
176
+ parser = argparse.ArgumentParser(description="Playground verification for Python gr2 exec status/run.")
177
+ parser.add_argument("--json", action="store_true")
178
+ args = parser.parse_args()
179
+
180
+ results = [scenario() for scenario in SCENARIOS]
181
+ verdict = "holds" if all(item["holds"] for item in results) else "gaps"
182
+ payload = {"verdict": verdict, "results": results}
183
+
184
+ if args.json:
185
+ print(json.dumps(payload, indent=2))
186
+ else:
187
+ print(f"python exec playground verdict: {verdict}")
188
+ for item in results:
189
+ print(f"- {item['name']}: {'holds' if item['holds'] else 'gaps'}")
190
+ return 0 if verdict == "holds" else 1
191
+
192
+
193
+ if __name__ == "__main__":
194
+ raise SystemExit(main())
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+
13
+
14
+ def run(args: list[str], *, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
15
+ env = os.environ.copy()
16
+ env["PYTHONPATH"] = str(ROOT)
17
+ proc = subprocess.run(args, cwd=cwd, env=env, text=True, capture_output=True)
18
+ if check and proc.returncode != 0:
19
+ raise SystemExit(f"command failed: {args}\nstdout={proc.stdout}\nstderr={proc.stderr}")
20
+ return proc
21
+
22
+
23
+ def pygr2(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
24
+ return run(["python3", "-m", "gr2.python_cli", *args], cwd=cwd, check=check)
25
+
26
+
27
+ def git(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
28
+ return run(["git", *args], cwd=cwd, check=check)
29
+
30
+
31
+ def seed_remote(remote: Path) -> None:
32
+ git("init", "--bare", str(remote))
33
+ with tempfile.TemporaryDirectory(prefix="gr2-hooks-seed-") as td:
34
+ seed = Path(td) / "seed"
35
+ git("clone", str(remote), str(seed))
36
+ git("config", "user.name", "Atlas", cwd=seed)
37
+ git("config", "user.email", "atlas@example.com", cwd=seed)
38
+ (seed / "README.md").write_text("# demo\n")
39
+ hooks_dir = seed / ".gr2"
40
+ hooks_dir.mkdir(parents=True, exist_ok=True)
41
+ (hooks_dir / "hooks.toml").write_text(
42
+ "\n".join(
43
+ [
44
+ "[[files.copy]]",
45
+ 'src = "{workspace_root}/shared.txt"',
46
+ 'dest = "{repo_root}/COPY_SKIP.txt"',
47
+ 'if_exists = "skip"',
48
+ "",
49
+ "[[files.copy]]",
50
+ 'src = "{workspace_root}/shared.txt"',
51
+ 'dest = "{repo_root}/COPY_OVERWRITE.txt"',
52
+ 'if_exists = "overwrite"',
53
+ "",
54
+ "[[lifecycle.on_enter]]",
55
+ 'name = "manual-write"',
56
+ "command = \"python3 -c \\\"from pathlib import Path; Path('MANUAL.txt').write_text('manual\\\\n')\\\"\"",
57
+ 'cwd = "{repo_root}"',
58
+ 'when = "manual"',
59
+ 'on_failure = "warn"',
60
+ "",
61
+ "[[lifecycle.on_enter]]",
62
+ 'name = "warn-fail"',
63
+ 'command = "python3 -c \\"import sys; sys.exit(3)\\""',
64
+ 'cwd = "{repo_root}"',
65
+ 'when = "always"',
66
+ 'on_failure = "warn"',
67
+ "",
68
+ "[[lifecycle.on_enter]]",
69
+ 'name = "skip-fail"',
70
+ 'command = "python3 -c \\"import sys; sys.exit(5)\\""',
71
+ 'cwd = "{repo_root}"',
72
+ 'when = "always"',
73
+ 'on_failure = "skip"',
74
+ "",
75
+ "[[lifecycle.on_exit]]",
76
+ 'name = "block-fail"',
77
+ 'command = "python3 -c \\"import sys; sys.exit(7)\\""',
78
+ 'cwd = "{repo_root}"',
79
+ 'when = "always"',
80
+ 'on_failure = "block"',
81
+ "",
82
+ ]
83
+ )
84
+ )
85
+ git("add", "README.md", ".gr2/hooks.toml", cwd=seed)
86
+ git("commit", "-m", "seed", cwd=seed)
87
+ git("push", "origin", "HEAD:main", cwd=seed)
88
+ git("--git-dir", str(remote), "symbolic-ref", "HEAD", "refs/heads/main")
89
+
90
+
91
+ def write_spec(workspace_root: Path, remote: Path) -> None:
92
+ (workspace_root / ".grip").mkdir(parents=True, exist_ok=True)
93
+ (workspace_root / ".grip" / "workspace_spec.toml").write_text(
94
+ "\n".join(
95
+ [
96
+ 'workspace_name = "hooks-playground"',
97
+ "",
98
+ "[[repos]]",
99
+ 'name = "demo"',
100
+ 'path = "repos/demo"',
101
+ f'url = "{remote.as_posix()}"',
102
+ "",
103
+ "[[units]]",
104
+ 'name = "atlas"',
105
+ 'path = "agents/atlas/home"',
106
+ 'repos = ["demo"]',
107
+ "",
108
+ ]
109
+ )
110
+ )
111
+
112
+
113
+ def base_workspace() -> Path:
114
+ workspace_root = Path(tempfile.mkdtemp(prefix="gr2-hooks-playground-"))
115
+ (workspace_root / "shared.txt").write_text("shared\n")
116
+ remote = workspace_root / "demo-remote.git"
117
+ seed_remote(remote)
118
+ write_spec(workspace_root, remote)
119
+ pygr2("apply", str(workspace_root), "--yes")
120
+ repo_root = workspace_root / "repos" / "demo"
121
+ repo_root.joinpath("COPY_SKIP.txt").write_text("original\n")
122
+ repo_root.joinpath("COPY_OVERWRITE.txt").write_text("old\n")
123
+ pygr2(
124
+ "lane",
125
+ "create",
126
+ str(workspace_root),
127
+ "atlas",
128
+ "feat-hooks",
129
+ "--repos",
130
+ "demo",
131
+ "--branch",
132
+ "feat/hooks",
133
+ )
134
+ return workspace_root
135
+
136
+
137
+ def scenario_manual_hook_flag() -> dict[str, object]:
138
+ workspace_root = base_workspace()
139
+ repo_root = workspace_root / "agents" / "atlas" / "lanes" / "feat-hooks" / "repos" / "demo"
140
+ pygr2("lane", "enter", str(workspace_root), "atlas", "feat-hooks", "--actor", "agent:atlas")
141
+ without_manual = not repo_root.joinpath("MANUAL.txt").exists()
142
+ with_manual = json.loads(
143
+ pygr2(
144
+ "repo",
145
+ "hook-run",
146
+ str(workspace_root),
147
+ str(repo_root),
148
+ "on_enter",
149
+ "--manual",
150
+ "--json",
151
+ ).stdout
152
+ )
153
+ return {
154
+ "name": "manual-hook-flag",
155
+ "holds": without_manual
156
+ and repo_root.joinpath("MANUAL.txt").exists()
157
+ and any(item["name"] == "manual-write" and item["status"] == "applied" for item in with_manual["results"]),
158
+ "payload": with_manual,
159
+ }
160
+
161
+
162
+ def scenario_warn_skip_and_block() -> dict[str, object]:
163
+ workspace_root = base_workspace()
164
+ repo_root = workspace_root / "agents" / "atlas" / "lanes" / "feat-hooks" / "repos" / "demo"
165
+ warn_skip = json.loads(
166
+ pygr2(
167
+ "repo",
168
+ "hook-run",
169
+ str(workspace_root),
170
+ str(repo_root),
171
+ "on_enter",
172
+ "--json",
173
+ ).stdout
174
+ )
175
+ block = pygr2(
176
+ "repo",
177
+ "hook-run",
178
+ str(workspace_root),
179
+ str(repo_root),
180
+ "on_exit",
181
+ "--json",
182
+ check=False,
183
+ )
184
+ return {
185
+ "name": "warn-skip-block",
186
+ "holds": any(item["name"] == "warn-fail" and item["status"] == "warned" for item in warn_skip["results"])
187
+ and any(item["name"] == "skip-fail" and item["status"] == "skipped" for item in warn_skip["results"])
188
+ and block.returncode != 0
189
+ and '"on_failure": "block"' in (block.stderr + block.stdout),
190
+ }
191
+
192
+
193
+ def scenario_projection_if_exists() -> dict[str, object]:
194
+ workspace_root = base_workspace()
195
+ repo_root = workspace_root / "repos" / "demo"
196
+ payload = json.loads(
197
+ pygr2(
198
+ "repo",
199
+ "projection-run",
200
+ str(workspace_root),
201
+ str(repo_root),
202
+ "--json",
203
+ ).stdout
204
+ )
205
+ return {
206
+ "name": "projection-if-exists",
207
+ "holds": repo_root.joinpath("COPY_SKIP.txt").read_text() == "original\n"
208
+ and repo_root.joinpath("COPY_OVERWRITE.txt").read_text() == "shared\n"
209
+ and any(item["status"] == "skipped" for item in payload["results"])
210
+ and any(item["status"] == "applied" for item in payload["results"]),
211
+ }
212
+
213
+
214
+ SCENARIOS = [
215
+ scenario_manual_hook_flag,
216
+ scenario_warn_skip_and_block,
217
+ scenario_projection_if_exists,
218
+ ]
219
+
220
+
221
+ def main() -> int:
222
+ parser = argparse.ArgumentParser(description="Playground verification for Python gr2 hook runtime semantics.")
223
+ parser.add_argument("--json", action="store_true")
224
+ args = parser.parse_args()
225
+
226
+ results = [scenario() for scenario in SCENARIOS]
227
+ verdict = "holds" if all(item["holds"] for item in results) else "gaps"
228
+ payload = {"verdict": verdict, "results": results}
229
+
230
+ if args.json:
231
+ print(json.dumps(payload, indent=2))
232
+ else:
233
+ print(f"python hook runtime verdict: {verdict}")
234
+ for item in results:
235
+ print(f"- {item['name']}: {'holds' if item['holds'] else 'gaps'}")
236
+ return 0 if verdict == "holds" else 1
237
+
238
+
239
+ if __name__ == "__main__":
240
+ raise SystemExit(main())
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+ import subprocess
9
+
10
+
11
+ ROOT = Path(__file__).resolve().parents[2]
12
+
13
+
14
+ def run(args: list[str], *, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
15
+ env = os.environ.copy()
16
+ env["PYTHONPATH"] = str(ROOT)
17
+ proc = subprocess.run(args, cwd=cwd, env=env, text=True, capture_output=True)
18
+ if check and proc.returncode != 0:
19
+ raise SystemExit(f"command failed: {args}\nstdout={proc.stdout}\nstderr={proc.stderr}")
20
+ return proc
21
+
22
+
23
+ def pygr2(*args: str, cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess[str]:
24
+ return run(["python3", "-m", "gr2.python_cli", *args], cwd=cwd, check=check)
25
+
26
+
27
+ def write_gr1_workspace(workspace_root: Path) -> None:
28
+ gitgrip = workspace_root / ".gitgrip"
29
+ (gitgrip / "spaces" / "main").mkdir(parents=True, exist_ok=True)
30
+ (gitgrip / "spaces" / "main" / "gripspace.yml").write_text(
31
+ "\n".join(
32
+ [
33
+ "version: 2",
34
+ "manifest:",
35
+ " url: git@github.com:synapt-dev/synapt-gripspace.git",
36
+ "repos:",
37
+ " grip:",
38
+ " url: git@github.com:synapt-dev/grip.git",
39
+ " path: ./gitgrip",
40
+ " revision: main",
41
+ " synapt:",
42
+ " url: git@github.com:synapt-dev/synapt.git",
43
+ " path: ./synapt",
44
+ " revision: main",
45
+ " mem0:",
46
+ " url: https://github.com/mem0ai/mem0.git",
47
+ " path: reference/mem0",
48
+ " default_branch: main",
49
+ " reference: true",
50
+ "",
51
+ ]
52
+ )
53
+ )
54
+ (gitgrip / "agents.toml").write_text(
55
+ "\n".join(
56
+ [
57
+ "[agents.atlas]",
58
+ 'worktree = "main"',
59
+ 'channel = "dev"',
60
+ "",
61
+ "[agents.apollo]",
62
+ 'worktree = "main"',
63
+ 'channel = "dev"',
64
+ "",
65
+ ]
66
+ )
67
+ )
68
+ (gitgrip / "state.json").write_text(json.dumps({"branchToPr": {"feat/auth": 123}}, indent=2))
69
+ (gitgrip / "sync-state.json").write_text(json.dumps({"timestamp": "2026-04-14T12:00:00Z"}, indent=2))
70
+ (gitgrip / "griptrees.json").write_text(json.dumps({"griptrees": {"review-pr1": {"path": "/tmp/review-pr1", "branch": "review-pr1"}}}, indent=2))
71
+
72
+
73
+ def scenario_detect_and_migrate() -> dict[str, object]:
74
+ with tempfile.TemporaryDirectory(prefix="gr2-migration-") as td:
75
+ workspace_root = Path(td)
76
+ write_gr1_workspace(workspace_root)
77
+ manifest_before = (workspace_root / ".gitgrip" / "spaces" / "main" / "gripspace.yml").read_text()
78
+
79
+ detect = json.loads(pygr2("workspace", "detect-gr1", str(workspace_root), "--json").stdout)
80
+ migrate = json.loads(pygr2("workspace", "migrate-gr1", str(workspace_root), "--json").stdout)
81
+ spec_text = (workspace_root / ".grip" / "workspace_spec.toml").read_text()
82
+ manifest_after = (workspace_root / ".gitgrip" / "spaces" / "main" / "gripspace.yml").read_text()
83
+
84
+ return {
85
+ "name": "detect-and-migrate-gr1",
86
+ "holds": detect["detected"] is True
87
+ and detect["repo_count"] == 3
88
+ and set(detect["agents"]) == {"apollo", "atlas"}
89
+ and "gitgrip" in spec_text
90
+ and 'name = "atlas"' in spec_text
91
+ and 'name = "apollo"' in spec_text
92
+ and (workspace_root / ".grip" / "migrations" / "gr1" / "state.json").exists()
93
+ and (workspace_root / ".grip" / "migrations" / "gr1" / "sync-state.json").exists()
94
+ and (workspace_root / ".grip" / "migrations" / "gr1" / "griptrees.json").exists()
95
+ and manifest_before == manifest_after,
96
+ "detect": detect,
97
+ "migrate": migrate,
98
+ }
99
+
100
+
101
+ def scenario_existing_gr2_blocks_without_force() -> dict[str, object]:
102
+ with tempfile.TemporaryDirectory(prefix="gr2-migration-force-") as td:
103
+ workspace_root = Path(td)
104
+ write_gr1_workspace(workspace_root)
105
+ grip_dir = workspace_root / ".grip"
106
+ grip_dir.mkdir(parents=True, exist_ok=True)
107
+ (grip_dir / "workspace_spec.toml").write_text('workspace_name = "existing"\n')
108
+ blocked = pygr2("workspace", "migrate-gr1", str(workspace_root), "--json", check=False)
109
+ forced = pygr2("workspace", "migrate-gr1", str(workspace_root), "--json", "--force")
110
+ forced_payload = json.loads(forced.stdout)
111
+ return {
112
+ "name": "migrate-force-guard",
113
+ "holds": blocked.returncode != 0
114
+ and "refusing to overwrite existing gr2 workspace spec" in (blocked.stderr + blocked.stdout)
115
+ and forced_payload["unit_count"] == 2,
116
+ }
117
+
118
+
119
+ SCENARIOS = [
120
+ scenario_detect_and_migrate,
121
+ scenario_existing_gr2_blocks_without_force,
122
+ ]
123
+
124
+
125
+ def main() -> int:
126
+ parser = argparse.ArgumentParser(description="Playground verification for gr1 -> Python gr2 migration.")
127
+ parser.add_argument("--json", action="store_true")
128
+ args = parser.parse_args()
129
+
130
+ results = [scenario() for scenario in SCENARIOS]
131
+ verdict = "holds" if all(item["holds"] for item in results) else "gaps"
132
+ payload = {"verdict": verdict, "results": results}
133
+
134
+ if args.json:
135
+ print(json.dumps(payload, indent=2))
136
+ else:
137
+ print(f"python migration playground verdict: {verdict}")
138
+ for item in results:
139
+ print(f"- {item['name']}: {'holds' if item['holds'] else 'gaps'}")
140
+ return 0 if verdict == "holds" else 1
141
+
142
+
143
+ if __name__ == "__main__":
144
+ raise SystemExit(main())