sase-github 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. sase_github-0.2.8/.release-please-manifest.json +3 -0
  2. {sase_github-0.2.6 → sase_github-0.2.8}/CHANGELOG.md +14 -0
  3. {sase_github-0.2.6 → sase_github-0.2.8}/PKG-INFO +1 -1
  4. {sase_github-0.2.6 → sase_github-0.2.8}/pyproject.toml +1 -1
  5. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/scripts/gh_setup.py +35 -6
  6. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/xprompts/gh.yml +1 -0
  7. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_gh_workspace_claims.py +183 -0
  8. sase_github-0.2.6/.release-please-manifest.json +0 -3
  9. {sase_github-0.2.6 → sase_github-0.2.8}/.github/workflows/ci.yml +0 -0
  10. {sase_github-0.2.6 → sase_github-0.2.8}/.github/workflows/pr-title.yml +0 -0
  11. {sase_github-0.2.6 → sase_github-0.2.8}/.github/workflows/publish.yml +0 -0
  12. {sase_github-0.2.6 → sase_github-0.2.8}/.sase_beads/beads.db +0 -0
  13. {sase_github-0.2.6 → sase_github-0.2.8}/.sase_beads/config.json +0 -0
  14. {sase_github-0.2.6 → sase_github-0.2.8}/.sase_beads/issues.jsonl +0 -0
  15. {sase_github-0.2.6 → sase_github-0.2.8}/CLAUDE.md +0 -0
  16. {sase_github-0.2.6 → sase_github-0.2.8}/Justfile +0 -0
  17. {sase_github-0.2.6 → sase_github-0.2.8}/LICENSE +0 -0
  18. {sase_github-0.2.6 → sase_github-0.2.8}/README.md +0 -0
  19. {sase_github-0.2.6 → sase_github-0.2.8}/docs/architecture.md +0 -0
  20. {sase_github-0.2.6 → sase_github-0.2.8}/docs/configuration.md +0 -0
  21. {sase_github-0.2.6 → sase_github-0.2.8}/docs/xprompts.md +0 -0
  22. {sase_github-0.2.6 → sase_github-0.2.8}/release-please-config.json +0 -0
  23. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/__init__.py +0 -0
  24. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/config.py +0 -0
  25. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/default_config.yml +0 -0
  26. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/errors.py +0 -0
  27. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/plugin.py +0 -0
  28. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/scripts/__init__.py +0 -0
  29. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/scripts/new_pr_desc_get_context.py +0 -0
  30. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/task_types.py +0 -0
  31. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/workspace_plugin.py +0 -0
  32. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/xprompts/new_pr_desc.yml +0 -0
  33. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/xprompts/pr_diff.yml +0 -0
  34. {sase_github-0.2.6 → sase_github-0.2.8}/src/sase_github/xprompts/prdd.yml +0 -0
  35. {sase_github-0.2.6 → sase_github-0.2.8}/tests/__init__.py +0 -0
  36. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_ci_install_contract.py +0 -0
  37. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_config.py +0 -0
  38. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_github_issues.py +0 -0
  39. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_github_plugin.py +0 -0
  40. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_github_pull_requests.py +0 -0
  41. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_submit_with_recorded_pr.py +0 -0
  42. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_task_types.py +0 -0
  43. {sase_github-0.2.6 → sase_github-0.2.8}/tests/test_workspace_plugin.py +0 -0
  44. {sase_github-0.2.6 → sase_github-0.2.8}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.8"
3
+ }
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.8](https://github.com/sase-org/sase-github/compare/v0.2.7...v0.2.8) (2026-08-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * **gh:** mark runner-bound workspace allocations ([f4663ae](https://github.com/sase-org/sase-github/commit/f4663ae526929b23378372005662c20b495bd0f7))
9
+
10
+ ## [0.2.7](https://github.com/sase-org/sase-github/compare/v0.2.6...v0.2.7) (2026-08-28)
11
+
12
+
13
+ ### Features
14
+
15
+ * **gh-setup:** adopt the runner's numbered workspace claim ([5e8e9ea](https://github.com/sase-org/sase-github/commit/5e8e9ea6a48b5b285a65d9cb1fa087f74d52b6b0))
16
+
3
17
  ## [0.2.6](https://github.com/sase-org/sase-github/compare/v0.2.5...v0.2.6) (2026-08-18)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sase-github
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: GitHub VCS plugin for sase
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sase-github"
7
- version = "0.2.6"
7
+ version = "0.2.8"
8
8
  description = "GitHub VCS plugin for sase"
9
9
  requires-python = ">=3.12"
10
10
  license = "MIT"
@@ -8,8 +8,10 @@ from sase.running_field import (
8
8
  WorkspaceClaim,
9
9
  claim_next_axe_workspace,
10
10
  claim_workspace,
11
+ find_runner_numbered_workspace,
11
12
  get_claimed_workspaces,
12
13
  release_workspace,
14
+ runner_has_placeholder_workspace,
13
15
  )
14
16
  from sase.sdd.store import materialize_sdd_store
15
17
  from sase.workspace_provider import resolve_ref
@@ -43,6 +45,7 @@ def main(
43
45
  # then incorrectly remove the RUNNING entry while the real workflow
44
46
  # (the parent process) is still alive.
45
47
  pid = os.getppid()
48
+ runner_bound_workspace = False
46
49
 
47
50
  # Skip claiming when pre-allocated — the launcher already claimed the workspace
48
51
  pre_allocated = os.environ.get("SASE_GH_PRE_ALLOCATED") == "1"
@@ -61,14 +64,25 @@ def main(
61
64
  pinned=not release,
62
65
  )
63
66
  else:
64
- workspace_num, workspace_dir = _claim_next_workspace(
67
+ adopted = _adopt_runner_workspace(
65
68
  project_file=project_file,
66
69
  primary_workspace_dir=resolved.primary_workspace_dir,
67
- workflow_name=workflow_name,
68
- pid=pid,
69
- cl_name=cl_name,
70
- pinned=not release,
71
70
  )
71
+ if adopted is not None:
72
+ workspace_num, workspace_dir = adopted
73
+ pre_allocated = True
74
+ else:
75
+ runner_bound_workspace = runner_has_placeholder_workspace(
76
+ project_file, pid=pid
77
+ )
78
+ workspace_num, workspace_dir = _claim_next_workspace(
79
+ project_file=project_file,
80
+ primary_workspace_dir=resolved.primary_workspace_dir,
81
+ workflow_name=workflow_name,
82
+ pid=pid,
83
+ cl_name=cl_name,
84
+ pinned=not release,
85
+ )
72
86
 
73
87
  # Refuse to hand this checkout to `prepare`/`checkout` if another live
74
88
  # agent still occupies it. Runs on the pre_allocated branch too: the
@@ -122,13 +136,28 @@ def main(
122
136
  print(f"checkout_target={resolved.checkout_target}")
123
137
  print(f"primary_workspace_dir={resolved.primary_workspace_dir}")
124
138
  # Don't release pre-allocated workspaces — the launcher handles that
125
- should_release = release and not pre_allocated
139
+ should_release = release and not pre_allocated and not runner_bound_workspace
126
140
  print(f"should_release={'true' if should_release else 'false'}")
141
+ print(f"runner_bound_workspace={'true' if runner_bound_workspace else 'false'}")
127
142
  print(f"_chdir={workspace_dir}")
128
143
  print(f"meta_workspace={workspace_num}")
129
144
  print(f"workflow_name={workflow_name}")
130
145
 
131
146
 
147
+ def _adopt_runner_workspace(
148
+ *,
149
+ project_file: str,
150
+ primary_workspace_dir: str,
151
+ ) -> tuple[int, str] | None:
152
+ """Reuse the calling runner's numbered pool claim, if it holds one."""
153
+ workspace_num = find_runner_numbered_workspace(project_file)
154
+ if workspace_num is None:
155
+ return None
156
+ workspace_dir = ensure_workspace_checkout(primary_workspace_dir, workspace_num)
157
+ materialize_sdd_store(workspace_dir, workspace_num)
158
+ return workspace_num, workspace_dir
159
+
160
+
132
161
  def _claim_next_workspace(
133
162
  *,
134
163
  project_file: str,
@@ -37,6 +37,7 @@ steps:
37
37
  checkout_target: word
38
38
  primary_workspace_dir: path
39
39
  should_release: bool
40
+ runner_bound_workspace: bool
40
41
  workflow_name: word
41
42
 
42
43
  - name: prepare
@@ -567,6 +567,189 @@ class TestGhSetupOccupancyGuard:
567
567
  ] == [10]
568
568
 
569
569
 
570
+ class TestGhSetupAdoptsRunnerWorkspace:
571
+ def test_adopts_parent_numbered_claim_without_second_claim(
572
+ self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
573
+ ) -> None:
574
+ parent_claim = WorkspaceClaim(
575
+ 12, "ace(run)-launcher", "feature", pid=os.getppid()
576
+ )
577
+ project_file = _write_project_file(tmp_path, running_claims=[parent_claim])
578
+ workspace_dir = tmp_path / "widget_12"
579
+ workspace_dir.mkdir()
580
+
581
+ with (
582
+ _isolate_ledger(tmp_path),
583
+ patch(
584
+ "sase_github.scripts.gh_setup.resolve_ref",
585
+ return_value=_resolved(project_file),
586
+ ),
587
+ patch.dict(os.environ, {"SASE_GH_PRE_ALLOCATED": "0"}),
588
+ patch(
589
+ "sase_github.scripts.gh_setup.ensure_workspace_checkout",
590
+ return_value=str(workspace_dir),
591
+ ),
592
+ patch("sase_github.scripts.gh_setup.materialize_sdd_store") as materialize,
593
+ patch(
594
+ "sase_github.scripts.gh_setup.claim_next_axe_workspace"
595
+ ) as claim_next,
596
+ patch("sase_github.scripts.gh_setup.claim_workspace") as claim,
597
+ ):
598
+ gh_setup.main(gh_ref="acme/widget", n=None, release=True)
599
+
600
+ claim_next.assert_not_called()
601
+ claim.assert_not_called()
602
+ materialize.assert_called_once_with(str(workspace_dir), 12)
603
+ out = capsys.readouterr().out
604
+ assert "workspace_num=12" in out
605
+ assert "should_release=false" in out
606
+ claims = get_claimed_workspaces(project_file)
607
+ assert [claim.workspace_num for claim in claims] == [12]
608
+ assert claims[0].pid == os.getppid()
609
+ assert read_occupant_record(str(workspace_dir)) is None
610
+
611
+ def test_parent_placeholder_allocation_is_runner_bound(
612
+ self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
613
+ ) -> None:
614
+ placeholder = WorkspaceClaim(
615
+ 0, "ace(run)-launcher", "feature", pid=os.getppid()
616
+ )
617
+ project_file = _write_project_file(tmp_path, running_claims=[placeholder])
618
+
619
+ with (
620
+ _isolate_ledger(tmp_path),
621
+ patch(
622
+ "sase_github.scripts.gh_setup.resolve_ref",
623
+ return_value=_resolved(project_file),
624
+ ),
625
+ patch.dict(os.environ, {"SASE_GH_PRE_ALLOCATED": "0"}),
626
+ patch(
627
+ "sase_github.scripts.gh_setup.ensure_workspace_checkout",
628
+ return_value="/work/widget_10/",
629
+ ),
630
+ patch("sase_github.scripts.gh_setup.materialize_sdd_store"),
631
+ ):
632
+ gh_setup.main(gh_ref="acme/widget", n=None, release=True)
633
+
634
+ claimed_nums = {
635
+ claim.workspace_num for claim in get_claimed_workspaces(project_file)
636
+ }
637
+ assert claimed_nums == {0, 10}
638
+ claims = get_claimed_workspaces(project_file)
639
+ allocated = next(claim for claim in claims if claim.workspace_num == 10)
640
+ assert allocated.pid == os.getppid()
641
+ out = capsys.readouterr().out
642
+ assert "workspace_num=10" in out
643
+ assert "should_release=false" in out
644
+ assert "runner_bound_workspace=true" in out
645
+
646
+ def test_pinned_n_does_not_adopt_parent_claim(
647
+ self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
648
+ ) -> None:
649
+ parent_claim = WorkspaceClaim(
650
+ 12, "ace(run)-launcher", "feature", pid=os.getppid()
651
+ )
652
+ project_file = _write_project_file(tmp_path, running_claims=[parent_claim])
653
+
654
+ with (
655
+ _isolate_ledger(tmp_path),
656
+ patch(
657
+ "sase_github.scripts.gh_setup.resolve_ref",
658
+ return_value=_resolved(project_file),
659
+ ),
660
+ patch.dict(os.environ, {"SASE_GH_PRE_ALLOCATED": "0"}),
661
+ patch(
662
+ "sase_github.scripts.gh_setup.ensure_workspace_checkout",
663
+ return_value="/work/widget_17/",
664
+ ),
665
+ patch("sase_github.scripts.gh_setup.materialize_sdd_store"),
666
+ ):
667
+ gh_setup.main(gh_ref="acme/widget", n=17, release=True)
668
+
669
+ claimed_nums = {
670
+ claim.workspace_num for claim in get_claimed_workspaces(project_file)
671
+ }
672
+ assert claimed_nums == {12, 17}
673
+ out = capsys.readouterr().out
674
+ assert "workspace_num=17" in out
675
+ assert "should_release=true" in out
676
+
677
+ def test_adopted_run_does_not_rewrite_occupant_record(self, tmp_path: Path) -> None:
678
+ parent_claim = WorkspaceClaim(
679
+ 12, "ace(run)-launcher", "feature", pid=os.getppid()
680
+ )
681
+ project_file = _write_project_file(tmp_path, running_claims=[parent_claim])
682
+ workspace_dir = tmp_path / "widget_12"
683
+ workspace_dir.mkdir()
684
+ original = new_occupant_record(
685
+ pid=os.getppid(),
686
+ workflow="ace(run)-launcher",
687
+ project="gh_acme__widget",
688
+ workspace_num=12,
689
+ agent_name="launcher-agent",
690
+ )
691
+ write_occupant_record(str(workspace_dir), original)
692
+
693
+ with (
694
+ _isolate_ledger(tmp_path),
695
+ patch(
696
+ "sase_github.scripts.gh_setup.resolve_ref",
697
+ return_value=_resolved(project_file),
698
+ ),
699
+ patch.dict(os.environ, {"SASE_GH_PRE_ALLOCATED": "0"}),
700
+ patch(
701
+ "sase_github.scripts.gh_setup.ensure_workspace_checkout",
702
+ return_value=str(workspace_dir),
703
+ ),
704
+ patch("sase_github.scripts.gh_setup.materialize_sdd_store"),
705
+ ):
706
+ gh_setup.main(gh_ref="acme/widget", n=None, release=True)
707
+
708
+ record = read_occupant_record(str(workspace_dir))
709
+ assert record is not None
710
+ assert record.pid == original.pid
711
+ assert record.workflow == "ace(run)-launcher"
712
+ assert record.agent_name == "launcher-agent"
713
+
714
+ def test_adopted_guard_refusal_releases_nothing(self, tmp_path: Path) -> None:
715
+ parent_claim = WorkspaceClaim(
716
+ 12, "ace(run)-launcher", "feature", pid=os.getppid()
717
+ )
718
+ project_file = _write_project_file(tmp_path, running_claims=[parent_claim])
719
+ workspace_dir = tmp_path / "widget_12"
720
+ workspace_dir.mkdir()
721
+ write_occupant_record(
722
+ str(workspace_dir),
723
+ new_occupant_record(
724
+ pid=os.getpid(),
725
+ workflow="ace(run)-rival",
726
+ project="gh_acme__widget",
727
+ workspace_num=12,
728
+ agent_name="rival-agent",
729
+ ),
730
+ )
731
+
732
+ with (
733
+ _isolate_ledger(tmp_path),
734
+ patch(
735
+ "sase_github.scripts.gh_setup.resolve_ref",
736
+ return_value=_resolved(project_file),
737
+ ),
738
+ patch.dict(os.environ, {"SASE_GH_PRE_ALLOCATED": "0"}),
739
+ patch(
740
+ "sase_github.scripts.gh_setup.ensure_workspace_checkout",
741
+ return_value=str(workspace_dir),
742
+ ),
743
+ patch("sase_github.scripts.gh_setup.materialize_sdd_store"),
744
+ pytest.raises(WorkspaceOccupiedError, match="rival-agent"),
745
+ ):
746
+ gh_setup.main(gh_ref="acme/widget", n=None, release=True)
747
+
748
+ assert [
749
+ claim.workspace_num for claim in get_claimed_workspaces(project_file)
750
+ ] == [12]
751
+
752
+
570
753
  class TestWsSubmitAtomicClaim:
571
754
  def test_submit_acquires_through_claim_next_dir_and_releases(
572
755
  self, tmp_path: Path
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.2.6"
3
- }
File without changes
File without changes
File without changes
File without changes
File without changes