gitgrip 2.0.0a1__tar.gz → 2.0.0a3__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.
- gitgrip-2.0.0a3/PKG-INFO +125 -0
- gitgrip-2.0.0a3/README.md +110 -0
- gitgrip-2.0.0a3/gitgrip.egg-info/PKG-INFO +125 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gitgrip.egg-info/SOURCES.txt +12 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/lane_workspace_prototype.py +129 -1
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/repo_maintenance_prototype.py +355 -7
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/pyproject.toml +4 -3
- gitgrip-2.0.0a3/python_cli/__main__.py +8 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/app.py +242 -19
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/commit.py +88 -2
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/execops.py +12 -1
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/git_review.py +48 -11
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/gitops.py +142 -13
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/grip.py +3 -1
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/grip_cli.py +5 -1
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/migration.py +29 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/review_run.py +45 -11
- gitgrip-2.0.0a3/python_cli/review_runners.py +292 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/spec_apply.py +6 -1
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/syncops.py +6 -1
- gitgrip-2.0.0a3/python_cli/workspace_guidance.py +22 -0
- gitgrip-2.0.0a3/tests/test_cli_adapter_error_boundary.py +226 -0
- gitgrip-2.0.0a3/tests/test_exec_apostrophe_and_oserror.py +279 -0
- gitgrip-2.0.0a3/tests/test_exec_single_string_and_materialize_plan.py +442 -0
- gitgrip-2.0.0a3/tests/test_first_five_minutes.py +56 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_git_review.py +93 -0
- gitgrip-2.0.0a3/tests/test_gr1_first_run_guidance.py +80 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_lane_create_fork_base_cli.py +7 -7
- gitgrip-2.0.0a3/tests/test_lane_create_refuses_own_branch.py +231 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_lane_lifecycle_safety.py +5 -0
- gitgrip-2.0.0a3/tests/test_lane_repo_paths_and_all_skipped.py +234 -0
- gitgrip-2.0.0a3/tests/test_migration_unadopted_notice.py +122 -0
- gitgrip-2.0.0a3/tests/test_readme_verbs_exist.py +151 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_cli.py +41 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_runners.py +168 -1
- gitgrip-2.0.0a3/tests/test_single_repo_cwd_refusal.py +158 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_version_static.py +2 -2
- gitgrip-2.0.0a3/tests/test_workspace_convert_clone.py +603 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_workspace_init_from_topology.py +2 -1
- gitgrip-2.0.0a3/tests/test_workspace_init_local_repos.py +120 -0
- gitgrip-2.0.0a1/PKG-INFO +0 -13
- gitgrip-2.0.0a1/README.md +0 -258
- gitgrip-2.0.0a1/gitgrip.egg-info/PKG-INFO +0 -13
- gitgrip-2.0.0a1/python_cli/__main__.py +0 -6
- gitgrip-2.0.0a1/python_cli/review_runners.py +0 -133
- gitgrip-2.0.0a1/tests/test_workspace_convert_clone.py +0 -211
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gitgrip.egg-info/dependency_links.txt +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gitgrip.egg-info/entry_points.txt +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gitgrip.egg-info/requires.txt +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gitgrip.egg-info/top_level.txt +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/__init__.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/__init__.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/activate.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/agent_manifest.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/cli.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/cross_repo.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/drivers.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/introspection.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/language_drivers.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/objects.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/perf.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/refs.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/trust.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/types.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/units.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/overlay/workspace_spec.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2/schemas/gr2-materialization-plan-v1.schema.json +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/gr2_overlay/__init__.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/__init__.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/cache_materialization_probe.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/concurrent_event_stress.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/concurrent_lease_stress.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/concurrent_workspace_cap_stress.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/contribution_protocol.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/cross_mode_lane_stress.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/jsonl_store.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/layout_model_probe.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/propagation_daemon.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/propagation_state_machine.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/python_exec_playground.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/python_hook_runtime_playground.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/python_migration_playground.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/python_review_checkout_playground.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/python_spec_apply_playground.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/real_git_lane_materialization.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/real_git_playground.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/recall_lane_history.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/prototypes/repo_transport_probe.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/__init__.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/add.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/branch.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/channel_bridge.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/clone_exec.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/config.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/env_exec.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/events.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/failures.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/file_exec.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/hooks.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/launch_exec.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/merge_verification.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/open_gr_review.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/platform.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/pr.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/project_review.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/prune.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/push.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/review.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/review_dispatch.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/review_ephemeral.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/review_rebind.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/staging_cleanup.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/target.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/python_cli/workspace_snapshot.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/setup.cfg +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_add.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_agent_manifest_overlay_source.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_app_event_outcomes.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_apply_convergence.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_branch.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_channel_bridge.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_clone_materialization.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_commit.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_config_base_path_guard.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_config_overlay.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_conftest_git_isolation.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_contribution_protocol.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_ensure_lane_checkout_reference_clone.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_env_materialization.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_event_log_integrity.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_event_outcome_policy.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_events.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_events_torn_line.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_execops.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_failure_event_outcome.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_foreground_launch_runtime.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_gr2_packaging.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_gr2_review_receipt.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_grip_cli.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_grip_hardening.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_grip_object_model.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_grip_snapshot.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_hook_events.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_hook_hardening.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_jsonl_store.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_lane_lease_show_json.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_lane_toml_serialization.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_launch_primitive.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_m2_overlay_hindsight.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_m2_overlay_mem0.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_m2_overlay_zep.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_materialization_plan_contract.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_merge_evidence.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_merge_parent_verification.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_migration.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_migration_overlay_compatibility.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_no_bare_python_cli_imports.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_null_current_lane_refusal.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_open_gr_enter.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_open_project_refusal_reason_cli.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_acceptance.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_activate.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_cli.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_cli_wiring.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_composition_merge.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_cross_repo_atomic_apply.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_cross_repo_perf_gate.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_cross_repo_rollback.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_driver_registry.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_introspection.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_multi_stack.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_object_encoding.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_perf_gates.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_python_driver.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_tier_b_encoding.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_tier_b_perf_gates.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_tier_b_repo_state.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_tier_b_staging_discipline.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_trust_model.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_overlay_workspace_spec.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_pr_events.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_pr_merge_method.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_pr_merge_partial_failure.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_project_file_materialization.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_project_review_range_reconstruction.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_project_review_three_repo.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_projection_tracked_dest.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_propagation_contribution.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_propagation_daemon.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_propagation_state_machine.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_prune.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_push.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_repo_status_linked_worktree.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_repo_status_schema_version.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_repo_status_symlinked_git_dir.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_bind_fork_base.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_bind_from_range.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_bind_verify.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_close_gr.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_create_project_cli.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_dispatch.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_ephemeral.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_open_lane.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_rebind.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_run.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_run_import_isolation.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_run_rerun_and_refusal_receipt.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_review_verify_inside.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_rmtree_or_refuse.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_snapshot_id_prefix_resolution.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_spec_apply_event_outcomes.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_sprint21_sync_platform.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_staging_cleanup.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_store_verb_rename.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_sync_event_outcomes.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_sync_fast_forward.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_sync_fetch.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_sync_seed_local.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_sync_ssh_rewrite.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_sync_status_probe.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_target.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_tmux_launch_runtime.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_unit_abort.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_unit_apply.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_unit_apply_dry_run.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_unit_manifest_schema.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_unit_propose.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_unit_topological_apply.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_workspace_edit_lease_cap.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_workspace_identity_boundary.py +0 -0
- {gitgrip-2.0.0a1 → gitgrip-2.0.0a3}/tests/test_workspace_snapshot.py +0 -0
gitgrip-2.0.0a3/PKG-INFO
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitgrip
|
|
3
|
+
Version: 2.0.0a3
|
|
4
|
+
Summary: Workspace layer for multi-repo work: workspaces, isolated lanes, and one grouped review per slice
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer>=0.9
|
|
8
|
+
Requires-Dist: tomli_w>=1.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: toml>=0.10
|
|
11
|
+
Requires-Dist: jsonschema>=4.18
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# gr2
|
|
17
|
+
|
|
18
|
+
**gr2 is the workspace layer for multi-repo work.** One repository is just git; when a change spans repositories — a feature slice, a release, several agents touching several repos — gr2 gives you a workspace over them, isolated lanes to work in, and one grouped review/PR set for the slice. It is the Python successor to the Rust `gr` (1.x, called gr1 below); both ship from this repository, and `gr2` is the command for the workspace and the slice.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv tool install --pre gitgrip # the PyPI package is "gitgrip"; the command is "gr2"
|
|
24
|
+
gr2 --version
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Without uv, a venv and pip do the same:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m venv ~/gr2-venv && source ~/gr2-venv/bin/activate
|
|
31
|
+
pip install --pre gitgrip
|
|
32
|
+
gr2 --version
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`--pre` is required while every gr2 release is an alpha: 1.5.0 and 1.5.1 were pre-alpha builds and are yanked, so without `--pre` there is nothing to install.
|
|
36
|
+
|
|
37
|
+
Already installed 1.5.0 or 1.5.1? The install commands above leave it in place, because an installed version already satisfies them. Upgrade explicitly: `uv tool upgrade --prerelease allow gitgrip`, or `pip install --pre -U gitgrip`. To run unreleased development code instead, install from a checkout: `pip install -e gr2/` from the repository root.
|
|
38
|
+
|
|
39
|
+
The Rust `gr` (gr1, 1.x) installs with `brew install synapt-dev/tap/gitgrip` or `cargo install gitgrip`. The two do not collide; you can have both installed.
|
|
40
|
+
|
|
41
|
+
## First five minutes
|
|
42
|
+
|
|
43
|
+
Every command below was run, in this order, in a clean virtualenv against a throwaway workspace of two repositories. Put two clones side by side in one folder, say `~/ws/repo-a` and `~/ws/repo-b`.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd ~/ws
|
|
47
|
+
gr2 workspace init ~/ws # scans the folder; writes .grip/workspace_spec.toml
|
|
48
|
+
gr2 workspace materialize ~/ws --yes # builds the .grip layout and a default unit
|
|
49
|
+
gr2 workspace status ~/ws # what kind of workspace this is
|
|
50
|
+
gr2 store init ~/ws # the workspace snapshot store (needed before review)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A **unit** is one worker — you, or one agent — and `default` is created for you. A **lane** is one piece of work spanning repos, with its own clones, so two lanes never step on each other:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gr2 lane create ~/ws default feat-x --repos repo-a,repo-b --branch feat/x
|
|
57
|
+
gr2 lane enter ~/ws default feat-x --actor human:you
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Both commands print, per repo, the absolute directory you work in:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
repo-a: ~/ws/.grip/state/lanes/default/feat-x/repos/repo-a
|
|
64
|
+
repo-b: ~/ws/.grip/state/lanes/default/feat-x/repos/repo-b
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Edit there — not in your original clones. Stage and commit across the lane's repos:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
gr2 add . --repo-path ~/ws/.grip/state/lanes/default/feat-x/repos/repo-a
|
|
71
|
+
gr2 commit -m "my change" --workspace-root ~/ws --owner-unit default
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`commit` with the lane flags commits every lane repo that has staged changes and names the repos it skipped. If every repo was skipped it exits non-zero with one sentence naming where it looked — and, when it can see them, where your staged changes actually are. Run one command across every lane repo, then turn the lane into one reviewable object:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
gr2 exec run ~/ws default --actor human:you -- pytest -q # one command, every lane repo
|
|
78
|
+
gr2 review create-project ~/ws default feat-x # prints gr:<sha>: one object to review
|
|
79
|
+
gr2 lane current ~/ws default # where am I?
|
|
80
|
+
gr2 lane exit ~/ws default --actor human:you
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`exec run` and `lane exit` require `--actor`. Every group and verb takes `--help`; `gr2 --help` lists them all.
|
|
84
|
+
|
|
85
|
+
## What works today, and what is not there yet
|
|
86
|
+
|
|
87
|
+
Alpha 2 is a working local workspace layer. Verified by running the walk above:
|
|
88
|
+
|
|
89
|
+
- `workspace` (init, materialize, status, migrate-gr1 from a gr1 gripspace), `lane` (create, enter, exit, current, lease), `store` (init, snapshot, log, diff, checkout), `exec run`, `repo status`, `sync status`, `review create-project`.
|
|
90
|
+
- Lanes are independent clones: two lanes, or two agents, never share refs, an index, or a working tree.
|
|
91
|
+
- A lane commit that commits nothing anywhere refuses loudly instead of reporting success.
|
|
92
|
+
|
|
93
|
+
Not there yet:
|
|
94
|
+
|
|
95
|
+
- The full walk with **remote** PRs (`gr2 push`, `gr2 pr create`) is documented in `--help` but was not exercised in a clean environment for this README; treat those two as alpha.
|
|
96
|
+
- `gr2 --help` and some verb output still say "prototype" in places; the wording is being cleaned up as verbs land.
|
|
97
|
+
- The Rust `gr` (gr1) still owns `spawn`, `fleet`, and `manifest` until their gr2 ports land. Everything else — workspace sync, lanes, the multi-repo slice, its review — is gr2's job; if a verb is missing or wrong there, that is a bug worth reporting, not a reason to reach for gr1.
|
|
98
|
+
|
|
99
|
+
## Commands
|
|
100
|
+
|
|
101
|
+
| Group | Verbs |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `workspace` | init, init-from-topology, materialize, status, convert-clone, detect-gr1, migrate-gr1, migrate-lane-state, bootstrap-gr1 |
|
|
104
|
+
| `spec` | show, validate |
|
|
105
|
+
| `lane` | create, enter, exit, current, resolve, bind, lease |
|
|
106
|
+
| (top level) | branch, add, commit, push, prune |
|
|
107
|
+
| `sync` | status, run |
|
|
108
|
+
| `pr` | create, status, checks, merge |
|
|
109
|
+
| `review` | open, close, checkout-pr, run, requirements, bind, verify, rebind, create-project |
|
|
110
|
+
| `exec` | status, run |
|
|
111
|
+
| `repo` | status, hooks, hook-run, projection-run |
|
|
112
|
+
| `store` | init, snapshot, log, diff, checkout |
|
|
113
|
+
| `target`, `config`, `plan`, `apply` | stored PR target; config overlays; show/apply the materialization plan |
|
|
114
|
+
|
|
115
|
+
## Overlay substrate
|
|
116
|
+
|
|
117
|
+
gr2 also carries the config-overlay substrate (capture, compose, and materialize configuration layers over a workspace): see [docs/OVERLAY-SUBSTRATE.md](docs/OVERLAY-SUBSTRATE.md).
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install -e "gr2[dev]" # from the repository root
|
|
123
|
+
cd gr2 && pytest # the gr2 suite (tests + overlay tests)
|
|
124
|
+
cd gr2 && ruff check . # lint, same config as CI
|
|
125
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# gr2
|
|
2
|
+
|
|
3
|
+
**gr2 is the workspace layer for multi-repo work.** One repository is just git; when a change spans repositories — a feature slice, a release, several agents touching several repos — gr2 gives you a workspace over them, isolated lanes to work in, and one grouped review/PR set for the slice. It is the Python successor to the Rust `gr` (1.x, called gr1 below); both ship from this repository, and `gr2` is the command for the workspace and the slice.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install --pre gitgrip # the PyPI package is "gitgrip"; the command is "gr2"
|
|
9
|
+
gr2 --version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Without uv, a venv and pip do the same:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
python3 -m venv ~/gr2-venv && source ~/gr2-venv/bin/activate
|
|
16
|
+
pip install --pre gitgrip
|
|
17
|
+
gr2 --version
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`--pre` is required while every gr2 release is an alpha: 1.5.0 and 1.5.1 were pre-alpha builds and are yanked, so without `--pre` there is nothing to install.
|
|
21
|
+
|
|
22
|
+
Already installed 1.5.0 or 1.5.1? The install commands above leave it in place, because an installed version already satisfies them. Upgrade explicitly: `uv tool upgrade --prerelease allow gitgrip`, or `pip install --pre -U gitgrip`. To run unreleased development code instead, install from a checkout: `pip install -e gr2/` from the repository root.
|
|
23
|
+
|
|
24
|
+
The Rust `gr` (gr1, 1.x) installs with `brew install synapt-dev/tap/gitgrip` or `cargo install gitgrip`. The two do not collide; you can have both installed.
|
|
25
|
+
|
|
26
|
+
## First five minutes
|
|
27
|
+
|
|
28
|
+
Every command below was run, in this order, in a clean virtualenv against a throwaway workspace of two repositories. Put two clones side by side in one folder, say `~/ws/repo-a` and `~/ws/repo-b`.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cd ~/ws
|
|
32
|
+
gr2 workspace init ~/ws # scans the folder; writes .grip/workspace_spec.toml
|
|
33
|
+
gr2 workspace materialize ~/ws --yes # builds the .grip layout and a default unit
|
|
34
|
+
gr2 workspace status ~/ws # what kind of workspace this is
|
|
35
|
+
gr2 store init ~/ws # the workspace snapshot store (needed before review)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A **unit** is one worker — you, or one agent — and `default` is created for you. A **lane** is one piece of work spanning repos, with its own clones, so two lanes never step on each other:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
gr2 lane create ~/ws default feat-x --repos repo-a,repo-b --branch feat/x
|
|
42
|
+
gr2 lane enter ~/ws default feat-x --actor human:you
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Both commands print, per repo, the absolute directory you work in:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
repo-a: ~/ws/.grip/state/lanes/default/feat-x/repos/repo-a
|
|
49
|
+
repo-b: ~/ws/.grip/state/lanes/default/feat-x/repos/repo-b
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Edit there — not in your original clones. Stage and commit across the lane's repos:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
gr2 add . --repo-path ~/ws/.grip/state/lanes/default/feat-x/repos/repo-a
|
|
56
|
+
gr2 commit -m "my change" --workspace-root ~/ws --owner-unit default
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`commit` with the lane flags commits every lane repo that has staged changes and names the repos it skipped. If every repo was skipped it exits non-zero with one sentence naming where it looked — and, when it can see them, where your staged changes actually are. Run one command across every lane repo, then turn the lane into one reviewable object:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gr2 exec run ~/ws default --actor human:you -- pytest -q # one command, every lane repo
|
|
63
|
+
gr2 review create-project ~/ws default feat-x # prints gr:<sha>: one object to review
|
|
64
|
+
gr2 lane current ~/ws default # where am I?
|
|
65
|
+
gr2 lane exit ~/ws default --actor human:you
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`exec run` and `lane exit` require `--actor`. Every group and verb takes `--help`; `gr2 --help` lists them all.
|
|
69
|
+
|
|
70
|
+
## What works today, and what is not there yet
|
|
71
|
+
|
|
72
|
+
Alpha 2 is a working local workspace layer. Verified by running the walk above:
|
|
73
|
+
|
|
74
|
+
- `workspace` (init, materialize, status, migrate-gr1 from a gr1 gripspace), `lane` (create, enter, exit, current, lease), `store` (init, snapshot, log, diff, checkout), `exec run`, `repo status`, `sync status`, `review create-project`.
|
|
75
|
+
- Lanes are independent clones: two lanes, or two agents, never share refs, an index, or a working tree.
|
|
76
|
+
- A lane commit that commits nothing anywhere refuses loudly instead of reporting success.
|
|
77
|
+
|
|
78
|
+
Not there yet:
|
|
79
|
+
|
|
80
|
+
- The full walk with **remote** PRs (`gr2 push`, `gr2 pr create`) is documented in `--help` but was not exercised in a clean environment for this README; treat those two as alpha.
|
|
81
|
+
- `gr2 --help` and some verb output still say "prototype" in places; the wording is being cleaned up as verbs land.
|
|
82
|
+
- The Rust `gr` (gr1) still owns `spawn`, `fleet`, and `manifest` until their gr2 ports land. Everything else — workspace sync, lanes, the multi-repo slice, its review — is gr2's job; if a verb is missing or wrong there, that is a bug worth reporting, not a reason to reach for gr1.
|
|
83
|
+
|
|
84
|
+
## Commands
|
|
85
|
+
|
|
86
|
+
| Group | Verbs |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `workspace` | init, init-from-topology, materialize, status, convert-clone, detect-gr1, migrate-gr1, migrate-lane-state, bootstrap-gr1 |
|
|
89
|
+
| `spec` | show, validate |
|
|
90
|
+
| `lane` | create, enter, exit, current, resolve, bind, lease |
|
|
91
|
+
| (top level) | branch, add, commit, push, prune |
|
|
92
|
+
| `sync` | status, run |
|
|
93
|
+
| `pr` | create, status, checks, merge |
|
|
94
|
+
| `review` | open, close, checkout-pr, run, requirements, bind, verify, rebind, create-project |
|
|
95
|
+
| `exec` | status, run |
|
|
96
|
+
| `repo` | status, hooks, hook-run, projection-run |
|
|
97
|
+
| `store` | init, snapshot, log, diff, checkout |
|
|
98
|
+
| `target`, `config`, `plan`, `apply` | stored PR target; config overlays; show/apply the materialization plan |
|
|
99
|
+
|
|
100
|
+
## Overlay substrate
|
|
101
|
+
|
|
102
|
+
gr2 also carries the config-overlay substrate (capture, compose, and materialize configuration layers over a workspace): see [docs/OVERLAY-SUBSTRATE.md](docs/OVERLAY-SUBSTRATE.md).
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install -e "gr2[dev]" # from the repository root
|
|
108
|
+
cd gr2 && pytest # the gr2 suite (tests + overlay tests)
|
|
109
|
+
cd gr2 && ruff check . # lint, same config as CI
|
|
110
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitgrip
|
|
3
|
+
Version: 2.0.0a3
|
|
4
|
+
Summary: Workspace layer for multi-repo work: workspaces, isolated lanes, and one grouped review per slice
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer>=0.9
|
|
8
|
+
Requires-Dist: tomli_w>=1.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: toml>=0.10
|
|
11
|
+
Requires-Dist: jsonschema>=4.18
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# gr2
|
|
17
|
+
|
|
18
|
+
**gr2 is the workspace layer for multi-repo work.** One repository is just git; when a change spans repositories — a feature slice, a release, several agents touching several repos — gr2 gives you a workspace over them, isolated lanes to work in, and one grouped review/PR set for the slice. It is the Python successor to the Rust `gr` (1.x, called gr1 below); both ship from this repository, and `gr2` is the command for the workspace and the slice.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv tool install --pre gitgrip # the PyPI package is "gitgrip"; the command is "gr2"
|
|
24
|
+
gr2 --version
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Without uv, a venv and pip do the same:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python3 -m venv ~/gr2-venv && source ~/gr2-venv/bin/activate
|
|
31
|
+
pip install --pre gitgrip
|
|
32
|
+
gr2 --version
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`--pre` is required while every gr2 release is an alpha: 1.5.0 and 1.5.1 were pre-alpha builds and are yanked, so without `--pre` there is nothing to install.
|
|
36
|
+
|
|
37
|
+
Already installed 1.5.0 or 1.5.1? The install commands above leave it in place, because an installed version already satisfies them. Upgrade explicitly: `uv tool upgrade --prerelease allow gitgrip`, or `pip install --pre -U gitgrip`. To run unreleased development code instead, install from a checkout: `pip install -e gr2/` from the repository root.
|
|
38
|
+
|
|
39
|
+
The Rust `gr` (gr1, 1.x) installs with `brew install synapt-dev/tap/gitgrip` or `cargo install gitgrip`. The two do not collide; you can have both installed.
|
|
40
|
+
|
|
41
|
+
## First five minutes
|
|
42
|
+
|
|
43
|
+
Every command below was run, in this order, in a clean virtualenv against a throwaway workspace of two repositories. Put two clones side by side in one folder, say `~/ws/repo-a` and `~/ws/repo-b`.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd ~/ws
|
|
47
|
+
gr2 workspace init ~/ws # scans the folder; writes .grip/workspace_spec.toml
|
|
48
|
+
gr2 workspace materialize ~/ws --yes # builds the .grip layout and a default unit
|
|
49
|
+
gr2 workspace status ~/ws # what kind of workspace this is
|
|
50
|
+
gr2 store init ~/ws # the workspace snapshot store (needed before review)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A **unit** is one worker — you, or one agent — and `default` is created for you. A **lane** is one piece of work spanning repos, with its own clones, so two lanes never step on each other:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gr2 lane create ~/ws default feat-x --repos repo-a,repo-b --branch feat/x
|
|
57
|
+
gr2 lane enter ~/ws default feat-x --actor human:you
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Both commands print, per repo, the absolute directory you work in:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
repo-a: ~/ws/.grip/state/lanes/default/feat-x/repos/repo-a
|
|
64
|
+
repo-b: ~/ws/.grip/state/lanes/default/feat-x/repos/repo-b
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Edit there — not in your original clones. Stage and commit across the lane's repos:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
gr2 add . --repo-path ~/ws/.grip/state/lanes/default/feat-x/repos/repo-a
|
|
71
|
+
gr2 commit -m "my change" --workspace-root ~/ws --owner-unit default
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`commit` with the lane flags commits every lane repo that has staged changes and names the repos it skipped. If every repo was skipped it exits non-zero with one sentence naming where it looked — and, when it can see them, where your staged changes actually are. Run one command across every lane repo, then turn the lane into one reviewable object:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
gr2 exec run ~/ws default --actor human:you -- pytest -q # one command, every lane repo
|
|
78
|
+
gr2 review create-project ~/ws default feat-x # prints gr:<sha>: one object to review
|
|
79
|
+
gr2 lane current ~/ws default # where am I?
|
|
80
|
+
gr2 lane exit ~/ws default --actor human:you
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`exec run` and `lane exit` require `--actor`. Every group and verb takes `--help`; `gr2 --help` lists them all.
|
|
84
|
+
|
|
85
|
+
## What works today, and what is not there yet
|
|
86
|
+
|
|
87
|
+
Alpha 2 is a working local workspace layer. Verified by running the walk above:
|
|
88
|
+
|
|
89
|
+
- `workspace` (init, materialize, status, migrate-gr1 from a gr1 gripspace), `lane` (create, enter, exit, current, lease), `store` (init, snapshot, log, diff, checkout), `exec run`, `repo status`, `sync status`, `review create-project`.
|
|
90
|
+
- Lanes are independent clones: two lanes, or two agents, never share refs, an index, or a working tree.
|
|
91
|
+
- A lane commit that commits nothing anywhere refuses loudly instead of reporting success.
|
|
92
|
+
|
|
93
|
+
Not there yet:
|
|
94
|
+
|
|
95
|
+
- The full walk with **remote** PRs (`gr2 push`, `gr2 pr create`) is documented in `--help` but was not exercised in a clean environment for this README; treat those two as alpha.
|
|
96
|
+
- `gr2 --help` and some verb output still say "prototype" in places; the wording is being cleaned up as verbs land.
|
|
97
|
+
- The Rust `gr` (gr1) still owns `spawn`, `fleet`, and `manifest` until their gr2 ports land. Everything else — workspace sync, lanes, the multi-repo slice, its review — is gr2's job; if a verb is missing or wrong there, that is a bug worth reporting, not a reason to reach for gr1.
|
|
98
|
+
|
|
99
|
+
## Commands
|
|
100
|
+
|
|
101
|
+
| Group | Verbs |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `workspace` | init, init-from-topology, materialize, status, convert-clone, detect-gr1, migrate-gr1, migrate-lane-state, bootstrap-gr1 |
|
|
104
|
+
| `spec` | show, validate |
|
|
105
|
+
| `lane` | create, enter, exit, current, resolve, bind, lease |
|
|
106
|
+
| (top level) | branch, add, commit, push, prune |
|
|
107
|
+
| `sync` | status, run |
|
|
108
|
+
| `pr` | create, status, checks, merge |
|
|
109
|
+
| `review` | open, close, checkout-pr, run, requirements, bind, verify, rebind, create-project |
|
|
110
|
+
| `exec` | status, run |
|
|
111
|
+
| `repo` | status, hooks, hook-run, projection-run |
|
|
112
|
+
| `store` | init, snapshot, log, diff, checkout |
|
|
113
|
+
| `target`, `config`, `plan`, `apply` | stored PR target; config overlays; show/apply the materialization plan |
|
|
114
|
+
|
|
115
|
+
## Overlay substrate
|
|
116
|
+
|
|
117
|
+
gr2 also carries the config-overlay substrate (capture, compose, and materialize configuration layers over a workspace): see [docs/OVERLAY-SUBSTRATE.md](docs/OVERLAY-SUBSTRATE.md).
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install -e "gr2[dev]" # from the repository root
|
|
123
|
+
cd gr2 && pytest # the gr2 suite (tests + overlay tests)
|
|
124
|
+
cd gr2 && ruff check . # lint, same config as CI
|
|
125
|
+
```
|
|
@@ -84,6 +84,7 @@ python_cli/spec_apply.py
|
|
|
84
84
|
python_cli/staging_cleanup.py
|
|
85
85
|
python_cli/syncops.py
|
|
86
86
|
python_cli/target.py
|
|
87
|
+
python_cli/workspace_guidance.py
|
|
87
88
|
python_cli/workspace_snapshot.py
|
|
88
89
|
tests/test_add.py
|
|
89
90
|
tests/test_agent_manifest_overlay_source.py
|
|
@@ -91,6 +92,7 @@ tests/test_app_event_outcomes.py
|
|
|
91
92
|
tests/test_apply_convergence.py
|
|
92
93
|
tests/test_branch.py
|
|
93
94
|
tests/test_channel_bridge.py
|
|
95
|
+
tests/test_cli_adapter_error_boundary.py
|
|
94
96
|
tests/test_clone_materialization.py
|
|
95
97
|
tests/test_commit.py
|
|
96
98
|
tests/test_config_base_path_guard.py
|
|
@@ -103,10 +105,14 @@ tests/test_event_log_integrity.py
|
|
|
103
105
|
tests/test_event_outcome_policy.py
|
|
104
106
|
tests/test_events.py
|
|
105
107
|
tests/test_events_torn_line.py
|
|
108
|
+
tests/test_exec_apostrophe_and_oserror.py
|
|
109
|
+
tests/test_exec_single_string_and_materialize_plan.py
|
|
106
110
|
tests/test_execops.py
|
|
107
111
|
tests/test_failure_event_outcome.py
|
|
112
|
+
tests/test_first_five_minutes.py
|
|
108
113
|
tests/test_foreground_launch_runtime.py
|
|
109
114
|
tests/test_git_review.py
|
|
115
|
+
tests/test_gr1_first_run_guidance.py
|
|
110
116
|
tests/test_gr2_packaging.py
|
|
111
117
|
tests/test_gr2_review_receipt.py
|
|
112
118
|
tests/test_grip_cli.py
|
|
@@ -117,8 +123,10 @@ tests/test_hook_events.py
|
|
|
117
123
|
tests/test_hook_hardening.py
|
|
118
124
|
tests/test_jsonl_store.py
|
|
119
125
|
tests/test_lane_create_fork_base_cli.py
|
|
126
|
+
tests/test_lane_create_refuses_own_branch.py
|
|
120
127
|
tests/test_lane_lease_show_json.py
|
|
121
128
|
tests/test_lane_lifecycle_safety.py
|
|
129
|
+
tests/test_lane_repo_paths_and_all_skipped.py
|
|
122
130
|
tests/test_lane_toml_serialization.py
|
|
123
131
|
tests/test_launch_primitive.py
|
|
124
132
|
tests/test_m2_overlay_hindsight.py
|
|
@@ -129,6 +137,7 @@ tests/test_merge_evidence.py
|
|
|
129
137
|
tests/test_merge_parent_verification.py
|
|
130
138
|
tests/test_migration.py
|
|
131
139
|
tests/test_migration_overlay_compatibility.py
|
|
140
|
+
tests/test_migration_unadopted_notice.py
|
|
132
141
|
tests/test_no_bare_python_cli_imports.py
|
|
133
142
|
tests/test_null_current_lane_refusal.py
|
|
134
143
|
tests/test_open_gr_enter.py
|
|
@@ -165,6 +174,7 @@ tests/test_propagation_daemon.py
|
|
|
165
174
|
tests/test_propagation_state_machine.py
|
|
166
175
|
tests/test_prune.py
|
|
167
176
|
tests/test_push.py
|
|
177
|
+
tests/test_readme_verbs_exist.py
|
|
168
178
|
tests/test_repo_status_linked_worktree.py
|
|
169
179
|
tests/test_repo_status_schema_version.py
|
|
170
180
|
tests/test_repo_status_symlinked_git_dir.py
|
|
@@ -184,6 +194,7 @@ tests/test_review_run_rerun_and_refusal_receipt.py
|
|
|
184
194
|
tests/test_review_runners.py
|
|
185
195
|
tests/test_review_verify_inside.py
|
|
186
196
|
tests/test_rmtree_or_refuse.py
|
|
197
|
+
tests/test_single_repo_cwd_refusal.py
|
|
187
198
|
tests/test_snapshot_id_prefix_resolution.py
|
|
188
199
|
tests/test_spec_apply_event_outcomes.py
|
|
189
200
|
tests/test_sprint21_sync_platform.py
|
|
@@ -208,4 +219,5 @@ tests/test_workspace_convert_clone.py
|
|
|
208
219
|
tests/test_workspace_edit_lease_cap.py
|
|
209
220
|
tests/test_workspace_identity_boundary.py
|
|
210
221
|
tests/test_workspace_init_from_topology.py
|
|
222
|
+
tests/test_workspace_init_local_repos.py
|
|
211
223
|
tests/test_workspace_snapshot.py
|
|
@@ -195,6 +195,7 @@ class LaneTransitionOutcome:
|
|
|
195
195
|
current_lane: str | None
|
|
196
196
|
state_path: Path
|
|
197
197
|
status: str = "ok"
|
|
198
|
+
repo_paths: dict[str, str] = dataclasses.field(default_factory=dict)
|
|
198
199
|
|
|
199
200
|
@property
|
|
200
201
|
def exit_code(self) -> int:
|
|
@@ -208,6 +209,7 @@ class LaneTransitionOutcome:
|
|
|
208
209
|
"previous_lane": self.previous_lane,
|
|
209
210
|
"current_lane": self.current_lane,
|
|
210
211
|
"state_path": str(self.state_path),
|
|
212
|
+
"repo_paths": dict(self.repo_paths),
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
|
|
@@ -921,6 +923,97 @@ def _validate_bound_worktree(bind_path: Path, workspace_root: Path) -> tuple[str
|
|
|
921
923
|
return head, branch
|
|
922
924
|
|
|
923
925
|
|
|
926
|
+
#: How long the origin may take to report its default branch before the answer is
|
|
927
|
+
#: "cannot tell". Generous for a symref read, and short enough that an origin which
|
|
928
|
+
#: accepts and never replies cannot block `lane create` with no output. Without it,
|
|
929
|
+
#: an offline laptop or a blackholed origin hangs the verb where it used to create a
|
|
930
|
+
#: lane without any network call at all.
|
|
931
|
+
_LS_REMOTE_TIMEOUT_SECONDS = 5.0
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _origin_default_branch(repo_path: Path, url: str | None = None) -> str | None:
|
|
935
|
+
"""The repo's integration branch, or None when it cannot be determined.
|
|
936
|
+
|
|
937
|
+
A clone records it as `origin/HEAD`. That symref is ABSENT in a clone made
|
|
938
|
+
before the remote had any refs — which is the normal state of a repo cloned
|
|
939
|
+
empty and pushed later, and it is exactly the state in which a lane created on
|
|
940
|
+
the default branch looks harmless. So when the local record is missing, ask the
|
|
941
|
+
origin itself (`ls-remote --symref`), which is the same remote the lane is about
|
|
942
|
+
to clone from anyway. A remote that cannot be reached gives None: "cannot tell"
|
|
943
|
+
is never allowed to become a refusal. Unreachable includes UNANSWERED — the call
|
|
944
|
+
is bounded, and an expiry is None on the same channel as any other git failure
|
|
945
|
+
rather than an exception the caller has to remember to catch separately.
|
|
946
|
+
"""
|
|
947
|
+
proc = gitops.git(repo_path, "rev-parse", "--abbrev-ref", "origin/HEAD")
|
|
948
|
+
if proc.returncode == 0:
|
|
949
|
+
ref = proc.stdout.strip()
|
|
950
|
+
if ref.startswith("origin/"):
|
|
951
|
+
return ref.split("/", 1)[1]
|
|
952
|
+
if not url:
|
|
953
|
+
return None
|
|
954
|
+
remote = gitops.git(
|
|
955
|
+
repo_path, "ls-remote", "--symref", url, "HEAD", timeout=_LS_REMOTE_TIMEOUT_SECONDS
|
|
956
|
+
)
|
|
957
|
+
if remote.returncode != 0:
|
|
958
|
+
# A git that never returned is the same answer as a git that refused:
|
|
959
|
+
# "cannot tell". The bound above is what makes that reachable at all — an
|
|
960
|
+
# origin that accepts and says nothing used to block the verb outright.
|
|
961
|
+
return None
|
|
962
|
+
for line in remote.stdout.splitlines():
|
|
963
|
+
if line.startswith("ref:") and line.rstrip().endswith("HEAD"):
|
|
964
|
+
ref = line.split()[1]
|
|
965
|
+
# Strip the PREFIX, never split on every slash: a default branch whose
|
|
966
|
+
# name contains one (`release/1.x`) came back as its last segment, so the
|
|
967
|
+
# guard compared the requested branch against a string the origin never
|
|
968
|
+
# reported and the no-isolation lane was created in silence.
|
|
969
|
+
if not ref.startswith("refs/heads/"):
|
|
970
|
+
return None
|
|
971
|
+
return ref.removeprefix("refs/heads/")
|
|
972
|
+
return None
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _lane_branch_refusal(workspace_root: Path, spec: dict, repo: str, branch: str) -> str | None:
|
|
976
|
+
"""The one sentence that refuses a lane branch with no isolation, or None.
|
|
977
|
+
|
|
978
|
+
A lane is the isolation primitive. A lane whose branch IS the repository's own
|
|
979
|
+
branch has no isolation at all: work committed there and pushed with `gr2 push`
|
|
980
|
+
lands on the branch the lane was supposed to be forked from, and the first
|
|
981
|
+
refusal arrived at `pr create` — after the remote had already been written
|
|
982
|
+
(measured on the released 2.0.0a2: `lane create --branch main`, push exit 0
|
|
983
|
+
"Pushed main", then `pr create` exit 1 "head branch main is the same as base
|
|
984
|
+
branch main"). Nothing refused at the point the choice was made.
|
|
985
|
+
|
|
986
|
+
The comparison is against what is CHECKED OUT here and what the origin says its
|
|
987
|
+
default is. A repo with no local checkout, or one whose origin cannot be
|
|
988
|
+
reached, is skipped rather than refused: this guard may not invent a conflict it
|
|
989
|
+
cannot see.
|
|
990
|
+
"""
|
|
991
|
+
entry = next((row for row in spec.get("repos", []) if row.get("name") == repo), None)
|
|
992
|
+
if entry is None:
|
|
993
|
+
return None
|
|
994
|
+
rel = entry.get("path")
|
|
995
|
+
if not rel:
|
|
996
|
+
return None
|
|
997
|
+
checkout = workspace_root / str(rel)
|
|
998
|
+
if not gitops.is_git_repo(checkout):
|
|
999
|
+
return None
|
|
1000
|
+
matched: str | None = None
|
|
1001
|
+
if branch == gitops.current_branch(checkout):
|
|
1002
|
+
matched = f"the branch checked out at {rel}"
|
|
1003
|
+
else:
|
|
1004
|
+
default = _origin_default_branch(checkout, str(entry.get("url") or "") or None)
|
|
1005
|
+
if default and branch == default:
|
|
1006
|
+
matched = f"the repo's integration branch ({default})"
|
|
1007
|
+
if matched is None:
|
|
1008
|
+
return None
|
|
1009
|
+
return (
|
|
1010
|
+
f"lane branch '{branch}' for repo '{repo}' is {matched}: a lane forked from the branch it will "
|
|
1011
|
+
f"land on has no isolation, and a push would go straight to '{branch}'. Pass a lane branch that is "
|
|
1012
|
+
f"neither the repo's checked-out nor its integration branch (e.g. feat/<name>), or use --bind to "
|
|
1013
|
+
f"label an existing worktree."
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
|
|
924
1017
|
def create_lane(args: argparse.Namespace) -> int:
|
|
925
1018
|
workspace_root = args.workspace_root.resolve()
|
|
926
1019
|
validate_lane_path_component(args.owner_unit, "owner_unit")
|
|
@@ -961,6 +1054,14 @@ def create_lane(args: argparse.Namespace) -> int:
|
|
|
961
1054
|
branch_map = {repos[0]: branch}
|
|
962
1055
|
else:
|
|
963
1056
|
branch_map = parse_branch_arg(args.branch, repos)
|
|
1057
|
+
# A materialized lane's branch may not be the repo's own branch: the lane
|
|
1058
|
+
# would have no isolation and its push would land on the integration
|
|
1059
|
+
# branch. Refused HERE, where the choice is made, rather than at the first
|
|
1060
|
+
# `pr create` after the remote had been written.
|
|
1061
|
+
for _repo in repos:
|
|
1062
|
+
refusal = _lane_branch_refusal(workspace_root, spec, _repo, branch_map[_repo])
|
|
1063
|
+
if refusal:
|
|
1064
|
+
raise SystemExit(refusal)
|
|
964
1065
|
|
|
965
1066
|
# Fork base: record what the caller supplies, do not derive.
|
|
966
1067
|
# Each entry names a repo IN the lane and carries a 40-hex integration-branch tip.
|
|
@@ -1038,6 +1139,18 @@ def create_lane(args: argparse.Namespace) -> int:
|
|
|
1038
1139
|
(lane_root / "context").mkdir()
|
|
1039
1140
|
atomic_replace_text(metadata_path, expected)
|
|
1040
1141
|
print(metadata_path)
|
|
1142
|
+
# The human line names where the actor works, per repo, absolute: a
|
|
1143
|
+
# materialized lane's repos live under its lane tree, a bound lane's one
|
|
1144
|
+
# repo IS the bound worktree. (The stranger's F2 finding: enter/create
|
|
1145
|
+
# used to point at lane.toml only, so the actor never learned where the
|
|
1146
|
+
# lane's repos are.)
|
|
1147
|
+
if lane_kind == "bound" and bound_worktree:
|
|
1148
|
+
for repo in repos:
|
|
1149
|
+
print(f"{repo}: {bound_worktree}")
|
|
1150
|
+
else:
|
|
1151
|
+
lane_root = lane_dir(workspace_root, args.owner_unit, args.lane_name)
|
|
1152
|
+
for repo in repos:
|
|
1153
|
+
print(f"{repo}: {lane_root / 'repos' / repo}")
|
|
1041
1154
|
return 0
|
|
1042
1155
|
|
|
1043
1156
|
|
|
@@ -1222,6 +1335,17 @@ def enter_lane(args: argparse.Namespace) -> LaneTransitionOutcome:
|
|
|
1222
1335
|
previous_lane = current.get("lane_name")
|
|
1223
1336
|
previous.insert(0, current)
|
|
1224
1337
|
|
|
1338
|
+
lane_kind = lane_doc.get("lane_kind", "materialized")
|
|
1339
|
+
if lane_kind == "bound" and lane_doc.get("bound_worktree"):
|
|
1340
|
+
repo_paths = {
|
|
1341
|
+
r: str(lane_doc["bound_worktree"]) for r in lane_doc.get("repos", [])
|
|
1342
|
+
}
|
|
1343
|
+
else:
|
|
1344
|
+
lane_root = lane_dir(workspace_root, args.owner_unit, args.lane_name)
|
|
1345
|
+
repo_paths = {
|
|
1346
|
+
r: str(lane_root / "repos" / r) for r in lane_doc.get("repos", [])
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1225
1349
|
deduped: list[dict] = []
|
|
1226
1350
|
seen: set[tuple[str, str]] = set()
|
|
1227
1351
|
for item in previous:
|
|
@@ -1237,6 +1361,7 @@ def enter_lane(args: argparse.Namespace) -> LaneTransitionOutcome:
|
|
|
1237
1361
|
"lane_name": args.lane_name,
|
|
1238
1362
|
"lane_type": lane_doc["lane_type"],
|
|
1239
1363
|
"repos": lane_doc.get("repos", []),
|
|
1364
|
+
"repo_paths": repo_paths,
|
|
1240
1365
|
"actor": args.actor,
|
|
1241
1366
|
"entered_at": now_utc(),
|
|
1242
1367
|
},
|
|
@@ -1270,7 +1395,10 @@ def enter_lane(args: argparse.Namespace) -> LaneTransitionOutcome:
|
|
|
1270
1395
|
"timestamp": event["timestamp"],
|
|
1271
1396
|
},
|
|
1272
1397
|
)
|
|
1273
|
-
return LaneTransitionOutcome(
|
|
1398
|
+
return LaneTransitionOutcome(
|
|
1399
|
+
"enter", args.owner_unit, previous_lane, args.lane_name, path,
|
|
1400
|
+
repo_paths=repo_paths,
|
|
1401
|
+
)
|
|
1274
1402
|
|
|
1275
1403
|
|
|
1276
1404
|
def exit_lane(args: argparse.Namespace) -> LaneTransitionOutcome:
|