sase-github 0.2.4__tar.gz → 0.2.6__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 (45) hide show
  1. sase_github-0.2.6/.release-please-manifest.json +3 -0
  2. {sase_github-0.2.4 → sase_github-0.2.6}/CHANGELOG.md +20 -0
  3. {sase_github-0.2.4 → sase_github-0.2.6}/CLAUDE.md +1 -0
  4. {sase_github-0.2.4 → sase_github-0.2.6}/PKG-INFO +1 -1
  5. {sase_github-0.2.4 → sase_github-0.2.6}/README.md +4 -0
  6. {sase_github-0.2.4 → sase_github-0.2.6}/docs/architecture.md +5 -0
  7. {sase_github-0.2.4 → sase_github-0.2.6}/pyproject.toml +4 -1
  8. sase_github-0.2.6/src/sase_github/scripts/gh_setup.py +257 -0
  9. sase_github-0.2.6/src/sase_github/task_types.py +46 -0
  10. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/workspace_plugin.py +32 -21
  11. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/xprompts/gh.yml +3 -0
  12. sase_github-0.2.6/tests/test_gh_workspace_claims.py +771 -0
  13. sase_github-0.2.6/tests/test_task_types.py +45 -0
  14. {sase_github-0.2.4 → sase_github-0.2.6}/tests/test_workspace_plugin.py +29 -11
  15. sase_github-0.2.4/.release-please-manifest.json +0 -3
  16. sase_github-0.2.4/src/sase_github/scripts/gh_setup.py +0 -78
  17. {sase_github-0.2.4 → sase_github-0.2.6}/.github/workflows/ci.yml +0 -0
  18. {sase_github-0.2.4 → sase_github-0.2.6}/.github/workflows/pr-title.yml +0 -0
  19. {sase_github-0.2.4 → sase_github-0.2.6}/.github/workflows/publish.yml +0 -0
  20. {sase_github-0.2.4 → sase_github-0.2.6}/.sase_beads/beads.db +0 -0
  21. {sase_github-0.2.4 → sase_github-0.2.6}/.sase_beads/config.json +0 -0
  22. {sase_github-0.2.4 → sase_github-0.2.6}/.sase_beads/issues.jsonl +0 -0
  23. {sase_github-0.2.4 → sase_github-0.2.6}/Justfile +0 -0
  24. {sase_github-0.2.4 → sase_github-0.2.6}/LICENSE +0 -0
  25. {sase_github-0.2.4 → sase_github-0.2.6}/docs/configuration.md +0 -0
  26. {sase_github-0.2.4 → sase_github-0.2.6}/docs/xprompts.md +0 -0
  27. {sase_github-0.2.4 → sase_github-0.2.6}/release-please-config.json +0 -0
  28. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/__init__.py +0 -0
  29. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/config.py +0 -0
  30. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/default_config.yml +0 -0
  31. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/errors.py +0 -0
  32. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/plugin.py +0 -0
  33. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/scripts/__init__.py +0 -0
  34. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/scripts/new_pr_desc_get_context.py +0 -0
  35. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/xprompts/new_pr_desc.yml +0 -0
  36. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/xprompts/pr_diff.yml +0 -0
  37. {sase_github-0.2.4 → sase_github-0.2.6}/src/sase_github/xprompts/prdd.yml +0 -0
  38. {sase_github-0.2.4 → sase_github-0.2.6}/tests/__init__.py +0 -0
  39. {sase_github-0.2.4 → sase_github-0.2.6}/tests/test_ci_install_contract.py +0 -0
  40. {sase_github-0.2.4 → sase_github-0.2.6}/tests/test_config.py +0 -0
  41. {sase_github-0.2.4 → sase_github-0.2.6}/tests/test_github_issues.py +0 -0
  42. {sase_github-0.2.4 → sase_github-0.2.6}/tests/test_github_plugin.py +0 -0
  43. {sase_github-0.2.4 → sase_github-0.2.6}/tests/test_github_pull_requests.py +0 -0
  44. {sase_github-0.2.4 → sase_github-0.2.6}/tests/test_submit_with_recorded_pr.py +0 -0
  45. {sase_github-0.2.4 → sase_github-0.2.6}/uv.lock +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.6"
3
+ }
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.6](https://github.com/sase-org/sase-github/compare/v0.2.5...v0.2.6) (2026-08-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * claim GitHub workspaces atomically ([61dd36f](https://github.com/sase-org/sase-github/commit/61dd36fa1aef7ec71475608e19de3ddf91b67b74))
9
+ * **workspace:** guard gh workflow steps against occupied checkouts ([51347d4](https://github.com/sase-org/sase-github/commit/51347d4f0a256c80785e7e2633fa589259073481))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **workspace:** release the gh setup claim when the occupancy guard refuses ([99a6277](https://github.com/sase-org/sase-github/commit/99a6277ecceb47d702c42351623ed27fbed11822))
15
+
16
+ ## [0.2.5](https://github.com/sase-org/sase-github/compare/v0.2.4...v0.2.5) (2026-08-18)
17
+
18
+
19
+ ### Features
20
+
21
+ * register agent-uncreatable github task type ([a68b879](https://github.com/sase-org/sase-github/commit/a68b879ba9a8a0ca5dc6f253a0c66563f9a90d4b))
22
+
3
23
  ## [0.2.4](https://github.com/sase-org/sase-github/compare/v0.2.3...v0.2.4) (2026-08-11)
4
24
 
5
25
 
@@ -15,6 +15,7 @@ just check # lint + test
15
15
 
16
16
  ## Architecture
17
17
  - `src/sase_github/plugin.py` — GitHubPlugin class (extends `sase.vcs_provider.plugins._git_common.GitCommon`)
18
+ - `src/sase_github/task_types.py` — agent-uncreatable `github` task type via `sase_task_types`
18
19
  - `src/sase_github/xprompts/` — GitHub workflow YAML files discovered via `sase_xprompts` entry point
19
20
  - Depends on `sase>=0.1.3` for base classes, hookspec, and script modules
20
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sase-github
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: GitHub VCS plugin for sase
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -76,6 +76,10 @@ Requires `sase>=0.11.0` as a dependency. For GitHub Enterprise Server or self-ho
76
76
  - **`get_github_orgs()`** — Reads `github_orgs` from sase config to determine SSH vs HTTPS clone URLs for
77
77
  organizations/users with push access, and to seed local `#gh:` owner completion
78
78
 
79
+ ### Task type
80
+
81
+ - **`github`** — Agent-uncreatable task type registered through the `sase_task_types` entry point. Beads of this type are created by SASE's external issue mirror, not by agents.
82
+
79
83
  ### XPrompts
80
84
 
81
85
  | XPrompt | Description |
@@ -14,6 +14,11 @@ Registered in `pyproject.toml`:
14
14
  | `sase_workspace:github` | `GitHubWorkspacePlugin` | Workspace orchestration (ref resolution, submission, mail prep) |
15
15
  | `sase_xprompts:sase_github` | — | Makes `#gh`, `#new_pr_desc`, `#prdd` xprompts discoverable |
16
16
  | `sase_config:sase_github` | — | Contributes `default_config.yml` to the sase config chain |
17
+ | `sase_task_types:github` | `GITHUB_TASK_TYPES` | Agent-uncreatable `github` task type for mirrored issues |
18
+
19
+ ### GitHub task type (`task_types.py`)
20
+
21
+ Registers one `sase_task_types` spec: `task_type: github`, `agent_creatable: false`, no extra fields. Agents never create this type. SASE's external issue mirror stamps it on new mirrored task beads. This is not an `IssueType`; mirrored issues stay `task` beads with an `external_ref`.
17
22
 
18
23
  ### GitHubPlugin (`plugin.py`)
19
24
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sase-github"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "GitHub VCS plugin for sase"
9
9
  requires-python = ">=3.12"
10
10
  license = "MIT"
@@ -32,6 +32,9 @@ sase_github = "sase_github"
32
32
  [project.entry-points."sase_xprompts"]
33
33
  sase_github = "sase_github"
34
34
 
35
+ [project.entry-points."sase_task_types"]
36
+ github = "sase_github.task_types:GITHUB_TASK_TYPES"
37
+
35
38
  [tool.hatch.build.targets.wheel]
36
39
  packages = ["src/sase_github"]
37
40
 
@@ -0,0 +1,257 @@
1
+ """Setup step for the #gh xprompt workflow."""
2
+
3
+ import os
4
+ import sys
5
+
6
+ from sase.core.occupancy_guard import OccupancyCaller, ensure_workspace_not_occupied
7
+ from sase.running_field import (
8
+ WorkspaceClaim,
9
+ claim_next_axe_workspace,
10
+ claim_workspace,
11
+ get_claimed_workspaces,
12
+ release_workspace,
13
+ )
14
+ from sase.sdd.store import materialize_sdd_store
15
+ from sase.workspace_provider import resolve_ref
16
+ from sase.workspace_provider.occupant import new_occupant_record, write_occupant_record
17
+ from sase.workspace_provider.utils import ensure_workspace_checkout
18
+
19
+ _CALLER_TAG = "gh-setup"
20
+
21
+
22
+ def main(
23
+ *,
24
+ gh_ref: str,
25
+ n: int | None,
26
+ release: bool,
27
+ workflow_label: str | None = None,
28
+ ) -> None:
29
+ """Resolve GitHub ref, claim a workspace, and print config.
30
+
31
+ Prints key=value output for the workflow executor.
32
+ """
33
+ resolved = resolve_ref(gh_ref, "gh")
34
+
35
+ project_name = resolved.project_name
36
+ project_file = resolved.project_file
37
+ workflow_name = workflow_label or f"gh-{gh_ref}"
38
+ cl_name = None if "/" in gh_ref else gh_ref
39
+
40
+ # Use the parent process PID, not our own. This setup step runs as a
41
+ # short-lived subprocess (via ``subprocess.run``), so ``os.getpid()``
42
+ # would die immediately after setup. ``stale_running_cleanup`` would
43
+ # then incorrectly remove the RUNNING entry while the real workflow
44
+ # (the parent process) is still alive.
45
+ pid = os.getppid()
46
+
47
+ # Skip claiming when pre-allocated — the launcher already claimed the workspace
48
+ pre_allocated = os.environ.get("SASE_GH_PRE_ALLOCATED") == "1"
49
+ if pre_allocated:
50
+ workspace_num = int(os.environ["SASE_GH_WORKSPACE_NUM"])
51
+ workspace_dir = os.environ["SASE_GH_WORKSPACE_DIR"]
52
+ materialize_sdd_store(workspace_dir, workspace_num)
53
+ elif n is not None:
54
+ workspace_num, workspace_dir = _claim_pinned_workspace(
55
+ project_file=project_file,
56
+ primary_workspace_dir=resolved.primary_workspace_dir,
57
+ workspace_num=n,
58
+ workflow_name=workflow_name,
59
+ pid=pid,
60
+ cl_name=cl_name,
61
+ pinned=not release,
62
+ )
63
+ else:
64
+ workspace_num, workspace_dir = _claim_next_workspace(
65
+ project_file=project_file,
66
+ 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
+ )
72
+
73
+ # Refuse to hand this checkout to `prepare`/`checkout` if another live
74
+ # agent still occupies it. Runs on the pre_allocated branch too: the
75
+ # launcher already wrote its own occupant record, and the conflict
76
+ # decision recognises the caller's own lineage via the shared pid.
77
+ # A refusal here aborts the step, so the slot this run just claimed must
78
+ # go back — the workflow's `release` step never runs once `setup` fails.
79
+ # The pre_allocated branch claimed nothing, so it releases nothing.
80
+ try:
81
+ ensure_workspace_not_occupied(
82
+ workspace_dir,
83
+ project_file=project_file,
84
+ caller=OccupancyCaller(
85
+ pid=pid,
86
+ workspace_num=workspace_num,
87
+ project=project_name,
88
+ workflow=workflow_name,
89
+ ),
90
+ )
91
+ except Exception:
92
+ if not pre_allocated:
93
+ release_workspace(
94
+ project_file,
95
+ workspace_num,
96
+ workflow_name,
97
+ cl_name,
98
+ caller_tag=_CALLER_TAG,
99
+ )
100
+ raise
101
+
102
+ # Claim the checkout for this run. Workspace numbers 0/1 both mean
103
+ # "primary", never a real numbered workspace (see
104
+ # ``ensure_workspace_checkout``), and the pre_allocated branch's
105
+ # occupant record already belongs to the launcher.
106
+ if not pre_allocated and workspace_num > 1:
107
+ write_occupant_record(
108
+ workspace_dir,
109
+ new_occupant_record(
110
+ pid=pid,
111
+ workflow=workflow_name,
112
+ project=project_name,
113
+ workspace_num=workspace_num,
114
+ cl_name=cl_name,
115
+ ),
116
+ )
117
+
118
+ print(f"project_name={project_name}")
119
+ print(f"project_file={project_file}")
120
+ print(f"workspace_dir={workspace_dir}")
121
+ print(f"workspace_num={workspace_num}")
122
+ print(f"checkout_target={resolved.checkout_target}")
123
+ print(f"primary_workspace_dir={resolved.primary_workspace_dir}")
124
+ # Don't release pre-allocated workspaces — the launcher handles that
125
+ should_release = release and not pre_allocated
126
+ print(f"should_release={'true' if should_release else 'false'}")
127
+ print(f"_chdir={workspace_dir}")
128
+ print(f"meta_workspace={workspace_num}")
129
+ print(f"workflow_name={workflow_name}")
130
+
131
+
132
+ def _claim_next_workspace(
133
+ *,
134
+ project_file: str,
135
+ primary_workspace_dir: str,
136
+ workflow_name: str,
137
+ pid: int,
138
+ cl_name: str | None,
139
+ pinned: bool,
140
+ ) -> tuple[int, str]:
141
+ """Atomically allocate a slot, then materialize the checkout."""
142
+ workspace_num = claim_next_axe_workspace(
143
+ project_file,
144
+ workflow_name,
145
+ pid,
146
+ cl_name=cl_name,
147
+ pinned=pinned,
148
+ caller_tag=_CALLER_TAG,
149
+ )
150
+ workspace_dir = _materialize_or_release(
151
+ project_file=project_file,
152
+ primary_workspace_dir=primary_workspace_dir,
153
+ workspace_num=workspace_num,
154
+ workflow_name=workflow_name,
155
+ cl_name=cl_name,
156
+ )
157
+ return workspace_num, workspace_dir
158
+
159
+
160
+ def _claim_pinned_workspace(
161
+ *,
162
+ project_file: str,
163
+ primary_workspace_dir: str,
164
+ workspace_num: int,
165
+ workflow_name: str,
166
+ pid: int,
167
+ cl_name: str | None,
168
+ pinned: bool,
169
+ ) -> tuple[int, str]:
170
+ """Claim a pinned ``n=<num>`` target once. Occupied slots fail with the occupant named."""
171
+ claim_result = claim_workspace(
172
+ project_file,
173
+ workspace_num,
174
+ workflow_name,
175
+ pid,
176
+ cl_name,
177
+ pinned=pinned,
178
+ caller_tag=_CALLER_TAG,
179
+ )
180
+ if not claim_result.success:
181
+ occupant = _describe_workspace_occupant(project_file, workspace_num)
182
+ if occupant is not None:
183
+ print(
184
+ f"Pinned workspace #{workspace_num} is already claimed by {occupant}",
185
+ file=sys.stderr,
186
+ )
187
+ else:
188
+ print(
189
+ f"Pinned workspace #{workspace_num} could not be claimed: "
190
+ f"{claim_result.error or 'unknown reason'}",
191
+ file=sys.stderr,
192
+ )
193
+ sys.exit(1)
194
+ workspace_dir = _materialize_or_release(
195
+ project_file=project_file,
196
+ primary_workspace_dir=primary_workspace_dir,
197
+ workspace_num=workspace_num,
198
+ workflow_name=workflow_name,
199
+ cl_name=cl_name,
200
+ )
201
+ return workspace_num, workspace_dir
202
+
203
+
204
+ def _materialize_or_release(
205
+ *,
206
+ project_file: str,
207
+ primary_workspace_dir: str,
208
+ workspace_num: int,
209
+ workflow_name: str,
210
+ cl_name: str | None,
211
+ ) -> str:
212
+ """Materialize after a successful claim; release the slot if setup fails."""
213
+ try:
214
+ workspace_dir = ensure_workspace_checkout(primary_workspace_dir, workspace_num)
215
+ materialize_sdd_store(workspace_dir, workspace_num)
216
+ except Exception:
217
+ release_workspace(
218
+ project_file,
219
+ workspace_num,
220
+ workflow_name,
221
+ cl_name,
222
+ caller_tag=_CALLER_TAG,
223
+ )
224
+ raise
225
+ return workspace_dir
226
+
227
+
228
+ def _describe_workspace_occupant(project_file: str, workspace_num: int) -> str | None:
229
+ occupants = [
230
+ claim
231
+ for claim in get_claimed_workspaces(project_file)
232
+ if claim.workspace_num == workspace_num
233
+ ]
234
+ if not occupants:
235
+ return None
236
+ return "; ".join(_format_workspace_occupant(claim) for claim in occupants)
237
+
238
+
239
+ def _format_workspace_occupant(claim: WorkspaceClaim) -> str:
240
+ name = claim.cl_name or claim.workflow
241
+ liveness = "live" if _pid_is_alive(claim.pid) else "dead"
242
+ detail = f"{name} (pid {claim.pid}, {liveness}, workflow {claim.workflow}"
243
+ if claim.artifacts_timestamp:
244
+ detail += f", artifacts {claim.artifacts_timestamp}"
245
+ return detail + ")"
246
+
247
+
248
+ def _pid_is_alive(pid: int) -> bool:
249
+ if pid <= 0:
250
+ return False
251
+ try:
252
+ os.kill(pid, 0)
253
+ except ProcessLookupError:
254
+ return False
255
+ except PermissionError:
256
+ return True
257
+ return True
@@ -0,0 +1,46 @@
1
+ """Declarative ``github`` task-type spec for the sase-github plugin.
2
+
3
+ Mirrored GitHub issues are already ``task`` beads created by sase's external
4
+ issue mirror. This spec stamps that origin as an agent-uncreatable catalog
5
+ member; it does not add an ``IssueType.GITHUB``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from typing import Any
12
+
13
+ from sase.task_types import hookimpl
14
+
15
+ _TASK_TYPE_SPEC_SCHEMA_VERSION = 1
16
+
17
+ GITHUB_TASK_TYPE = "github"
18
+
19
+ GITHUB_TASK_TYPE_SPEC: Mapping[str, Any] = {
20
+ "schema_version": _TASK_TYPE_SPEC_SCHEMA_VERSION,
21
+ "task_type": GITHUB_TASK_TYPE,
22
+ "label": "GitHub",
23
+ "summary": (
24
+ "A GitHub issue mirrored into a task bead; agents never create this type."
25
+ ),
26
+ "when_to_use": (
27
+ "Agents never create this type. Beads of it are created by the "
28
+ "external issue mirror when it covers a GitHub issue. Do not file "
29
+ "one from `sase bead create` or `/sase_new_task`."
30
+ ),
31
+ "glyph": "⑂",
32
+ "accent_color": "#B2B2B2",
33
+ "agent_creatable": False,
34
+ "fields": [],
35
+ }
36
+
37
+
38
+ class _GitHubTaskTypes:
39
+ """Pluggy hookimpl exposing the ``github`` task-type spec."""
40
+
41
+ @hookimpl
42
+ def task_type_specs(self) -> tuple[Mapping[str, Any], ...]:
43
+ return (GITHUB_TASK_TYPE_SPEC,)
44
+
45
+
46
+ GITHUB_TASK_TYPES = _GitHubTaskTypes()
@@ -487,10 +487,14 @@ class GitHubWorkspacePlugin:
487
487
  kill_and_persist_all_running_processes,
488
488
  )
489
489
  from sase.ace.operations import has_active_children
490
+ from sase.core.occupancy_guard import (
491
+ OccupancyCaller,
492
+ WorkspaceOccupiedError,
493
+ ensure_workspace_not_occupied,
494
+ )
490
495
  from sase.running_field import (
491
- claim_workspace,
492
- get_first_available_axe_workspace,
493
- get_workspace_directory_for_num,
496
+ WorkspaceClaimError,
497
+ claim_next_axe_workspace_dir,
494
498
  release_workspace,
495
499
  )
496
500
  from sase.vcs_provider import get_vcs_provider
@@ -536,36 +540,42 @@ class GitHubWorkspacePlugin:
536
540
  if not workspace_dir:
537
541
  return (False, "WORKSPACE_DIR is not set for this project")
538
542
 
539
- workspace_num = get_first_available_axe_workspace(patch_file)
540
543
  workflow_name = f"submit-{patch_name}"
541
- pid = os.getpid()
542
-
543
544
  try:
544
- ws_dir, _ = get_workspace_directory_for_num(workspace_num, project_basename)
545
- except RuntimeError as e:
546
- return (False, f"Failed to get workspace directory: {e}")
545
+ workspace_num, ws_dir, _ = claim_next_axe_workspace_dir(
546
+ patch_file,
547
+ workflow_name,
548
+ os.getpid(),
549
+ project_basename,
550
+ cl_name=patch_name,
551
+ caller_tag="gh-submit",
552
+ )
553
+ except WorkspaceClaimError as exc:
554
+ return (False, f"Failed to claim workspace: {exc}")
547
555
 
548
556
  if rich_console:
549
557
  rich_console.print(f"[cyan]Claiming workspace #{workspace_num}[/cyan]")
550
558
 
551
- if not claim_workspace(
552
- patch_file,
553
- workspace_num,
554
- workflow_name,
555
- pid,
556
- patch_name,
557
- ):
558
- return (
559
- False,
560
- f"Failed to claim workspace #{workspace_num}",
561
- )
562
-
563
559
  try:
564
560
  if rich_console:
565
561
  rich_console.print(
566
562
  f"[cyan]Checking out {escape_markup(patch_name)}...[/cyan]"
567
563
  )
568
564
 
565
+ try:
566
+ ensure_workspace_not_occupied(
567
+ ws_dir,
568
+ project_file=patch_file,
569
+ caller=OccupancyCaller(
570
+ pid=os.getpid(),
571
+ workspace_num=workspace_num,
572
+ project=project_basename,
573
+ workflow=workflow_name,
574
+ ),
575
+ )
576
+ except WorkspaceOccupiedError as exc:
577
+ return (False, f"Refusing checkout: {exc}")
578
+
569
579
  provider = get_vcs_provider(ws_dir)
570
580
  branch_name = provider.resolve_revision(
571
581
  patch_name, project_basename, ws_dir
@@ -619,6 +629,7 @@ class GitHubWorkspacePlugin:
619
629
  workspace_num,
620
630
  workflow_name,
621
631
  patch_name,
632
+ caller_tag="gh-submit",
622
633
  )
623
634
  if rich_console:
624
635
  rich_console.print(f"[cyan]Released workspace #{workspace_num}[/cyan]")
@@ -120,13 +120,16 @@ steps:
120
120
  if: "{{ setup.should_release }}"
121
121
  python: |
122
122
  from sase.running_field import release_workspace
123
+ from sase.workspace_provider.occupant import clear_occupant_record
123
124
 
124
125
  release_workspace(
125
126
  {{ setup.project_file | tojson }},
126
127
  {{ setup.workspace_num }},
127
128
  {{ setup.workflow_name | tojson }},
128
129
  {{ gh_ref | tojson }} if "/" not in {{ gh_ref | tojson }} else None,
130
+ caller_tag="gh-release",
129
131
  )
132
+ clear_occupant_record({{ setup.workspace_dir | tojson }})
130
133
  print("released=true")
131
134
  output: { released: bool }
132
135