worktree-controller 1.30.4__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 (61) hide show
  1. worktree_controller/__init__.py +3 -0
  2. worktree_controller/activation_layout.py +483 -0
  3. worktree_controller/branch_retirement.py +1865 -0
  4. worktree_controller/canonical_refresh.py +150 -0
  5. worktree_controller/canonical_transition.py +324 -0
  6. worktree_controller/cleanup_attempts.py +442 -0
  7. worktree_controller/cleanup_evidence.py +75 -0
  8. worktree_controller/cleanup_recovery.py +912 -0
  9. worktree_controller/cli.py +1907 -0
  10. worktree_controller/controller.py +1974 -0
  11. worktree_controller/convergence.py +893 -0
  12. worktree_controller/delivered_closeout.py +928 -0
  13. worktree_controller/disposable.py +677 -0
  14. worktree_controller/environment.py +1038 -0
  15. worktree_controller/frozen_recovery.py +291 -0
  16. worktree_controller/git_backend.py +682 -0
  17. worktree_controller/governance.py +365 -0
  18. worktree_controller/hardlinks.py +294 -0
  19. worktree_controller/integration_cleanup.py +92 -0
  20. worktree_controller/integration_conflict.py +117 -0
  21. worktree_controller/integration_retry.py +526 -0
  22. worktree_controller/keyed_document_scope.py +162 -0
  23. worktree_controller/lifecycle.py +8992 -0
  24. worktree_controller/lifecycle_history.py +358 -0
  25. worktree_controller/lifecycle_observation.py +234 -0
  26. worktree_controller/metadata_codec.py +1694 -0
  27. worktree_controller/metadata_relocation.py +131 -0
  28. worktree_controller/models.py +546 -0
  29. worktree_controller/mount_diagnostics.py +324 -0
  30. worktree_controller/multiple_cleanup_recovery.py +730 -0
  31. worktree_controller/namespace_migration.py +1412 -0
  32. worktree_controller/native_session_recovery.py +171 -0
  33. worktree_controller/owner_actions.py +34 -0
  34. worktree_controller/policy.py +333 -0
  35. worktree_controller/preservation.py +983 -0
  36. worktree_controller/preserved_cleanup.py +849 -0
  37. worktree_controller/publication_execution.py +361 -0
  38. worktree_controller/reconciler.py +430 -0
  39. worktree_controller/recovery_guidance.py +205 -0
  40. worktree_controller/registration_backfill.py +166 -0
  41. worktree_controller/registration_prune.py +391 -0
  42. worktree_controller/rendering.py +179 -0
  43. worktree_controller/resource_admission.py +1736 -0
  44. worktree_controller/retirement_adapters.py +130 -0
  45. worktree_controller/retirement_execution.py +234 -0
  46. worktree_controller/semantics.py +459 -0
  47. worktree_controller/session.py +505 -0
  48. worktree_controller/session_admission.py +265 -0
  49. worktree_controller/source_cleanup.py +515 -0
  50. worktree_controller/source_preservation.py +692 -0
  51. worktree_controller/source_recovery.py +592 -0
  52. worktree_controller/store.py +1228 -0
  53. worktree_controller/tool_cli_contract.json +1791 -0
  54. worktree_controller/transaction_scope.py +876 -0
  55. worktree_controller/writer_lease.py +662 -0
  56. worktree_controller/writer_session.py +887 -0
  57. worktree_controller-1.30.4.dist-info/METADATA +3149 -0
  58. worktree_controller-1.30.4.dist-info/RECORD +61 -0
  59. worktree_controller-1.30.4.dist-info/WHEEL +4 -0
  60. worktree_controller-1.30.4.dist-info/entry_points.txt +5 -0
  61. worktree_controller-1.30.4.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,265 @@
1
+ """One-process admission of an executor session (``session_admission_version`` 1).
2
+
3
+ A host that launches a governed executor would otherwise run every step below as
4
+ its own Controller command: each one a new process that loads the repository
5
+ metadata again and attaches a full repository status to its answer. These two
6
+ commands run the same step functions, in the same order, with the same guards,
7
+ journals, receipts and CAS checks. They only share one process and skip the
8
+ status each intermediate step would otherwise attach.
9
+
10
+ ``session-admit`` covers the common start only: a new allocation acquired from
11
+ the canonical checkout, then execution admission. Every other start (an existing
12
+ allocation, a non-canonical workspace, a resumed checkout, a capacity decision
13
+ other than ``CREATE_ALLOWED``) is answered with ``SESSION_ADMISSION_FALLBACK`` and
14
+ nothing is changed, so the host keeps using its step-by-step path for it.
15
+
16
+ ``session-bind`` binds write entitlement to the live executor process: the lease
17
+ for the exact holder, the holder-aware write guard, then the runtime occupancy
18
+ binding. A failure after the lease was granted releases that same lease before
19
+ the error is returned, so a refused bind leaves no entitlement behind.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from .controller import _plan_from_snapshot, envelope, guard_write
29
+ from .git_backend import inventory
30
+ from .lifecycle import acquire, update_external_binding
31
+ from .models import ControllerError, WriterHolder
32
+ from .policy import load_policy
33
+ from .semantics import managed_record as _managed_record
34
+ from .semantics import record_for as _record_for
35
+ from .store import MetadataStore
36
+ from .writer_lease import RUNTIME_BINDING_PREFIX, acquire_writer_lease, release_writer_lease
37
+
38
+ SESSION_ADMISSION_VERSION = 1
39
+ RUNTIME_KINDS = frozenset({"agent", "command", "test"})
40
+
41
+
42
+ def _fallback(command: str, reason: str, **extra: Any) -> dict[str, Any]:
43
+ payload = envelope(command)
44
+ payload["decision"] = {
45
+ "code": "SESSION_ADMISSION_FALLBACK",
46
+ "reason": reason,
47
+ "changed": False,
48
+ **extra,
49
+ }
50
+ return payload
51
+
52
+
53
+ def admit_session(
54
+ repository_path: Path,
55
+ *,
56
+ task_key: str,
57
+ authority_key: str,
58
+ module: str,
59
+ owner: str,
60
+ branch: str,
61
+ base_ref: str,
62
+ client_id: str,
63
+ session_id: str,
64
+ run_id: str | None = None,
65
+ ) -> dict[str, Any]:
66
+ """Acquire a new feature allocation and admit its execution, or change nothing."""
67
+
68
+ command = "session-admit"
69
+ repository = inventory(repository_path)
70
+ requested = repository_path.expanduser().resolve(strict=True)
71
+ if requested != repository.canonical_path:
72
+ return _fallback(command, "workspace is not the canonical checkout")
73
+ snapshot = MetadataStore(repository.common_dir).load()
74
+ for item in repository.worktrees:
75
+ record = _record_for(item, snapshot)
76
+ if record is not None and record.task_key == task_key:
77
+ return _fallback(
78
+ command,
79
+ "an allocation for this task is already registered",
80
+ allocation_id=record.allocation_id,
81
+ )
82
+ planned = _plan_from_snapshot(
83
+ repository,
84
+ load_policy(repository.canonical_path),
85
+ snapshot,
86
+ task_key=task_key,
87
+ authority_key=authority_key,
88
+ module=module,
89
+ role="feature",
90
+ )
91
+ planned_decision = planned.get("decision")
92
+ if not isinstance(planned_decision, dict) or planned_decision.get("code") != "CREATE_ALLOWED":
93
+ return _fallback(command, "plan did not allow an immediate checkout", plan=planned_decision)
94
+ acquired = acquire(
95
+ repository_path,
96
+ task_key=task_key,
97
+ authority_key=authority_key,
98
+ module=module,
99
+ role="feature",
100
+ owner=owner,
101
+ branch=branch,
102
+ base_ref=base_ref,
103
+ observe=False,
104
+ )
105
+ decision = acquired["decision"]
106
+ if decision.get("code") != "ACQUIRED":
107
+ # RESOURCE_WAIT and similar outcomes keep their own durable request; the
108
+ # host handles them exactly as it handles a step-by-step acquire.
109
+ acquired["command"] = command
110
+ return acquired
111
+ allocation = {
112
+ "canonical_path": str(repository.canonical_path),
113
+ "path": decision["path"],
114
+ "allocation_id": decision["allocation_id"],
115
+ "task_key": task_key,
116
+ "authority_key": authority_key,
117
+ "owner": owner,
118
+ "role": "feature",
119
+ "lifecycle": "ACTIVE",
120
+ "branch": decision["branch"],
121
+ "head": decision["head"],
122
+ }
123
+ execution: dict[str, Any] | None = None
124
+ policy = load_policy(repository.canonical_path)
125
+ if policy.resource_admission:
126
+ from .resource_admission import admit_execution
127
+
128
+ execution = admit_execution(
129
+ repository_path,
130
+ target=Path(decision["path"]),
131
+ owner=owner,
132
+ client_id=client_id,
133
+ session_id=session_id,
134
+ run_id=run_id,
135
+ )["decision"]
136
+ payload = envelope(command)
137
+ payload["decision"] = {
138
+ "code": "SESSION_ADMITTED",
139
+ "reason": "new allocation acquired and its execution admitted"
140
+ if execution is not None
141
+ else "new allocation acquired",
142
+ "allocation": allocation,
143
+ "admission_strategy": "resource-budget-v1" if policy.resource_admission else None,
144
+ "execution": execution,
145
+ "session_admission_version": SESSION_ADMISSION_VERSION,
146
+ }
147
+ return payload
148
+
149
+
150
+ def runtime_binding_value(holder: dict[str, Any], lease_id: str, kind: str) -> str:
151
+ """The typed occupancy identity, in the exact form the registry decodes."""
152
+ identity = {
153
+ "holder": {
154
+ "pid": holder["pid"],
155
+ "boot_id": holder["boot_id"],
156
+ "start_ticks": holder["start_ticks"],
157
+ "client_id": holder["client_id"],
158
+ "session_id": holder["session_id"],
159
+ "run_id": holder["run_id"],
160
+ },
161
+ "lease_id": lease_id,
162
+ "kind": kind,
163
+ }
164
+ return RUNTIME_BINDING_PREFIX + json.dumps(identity, sort_keys=True, separators=(",", ":"))
165
+
166
+
167
+ def bind_session(
168
+ repository_path: Path,
169
+ *,
170
+ target: Path,
171
+ task_key: str,
172
+ authority_key: str,
173
+ owner: str,
174
+ client_id: str,
175
+ holder_pid: int,
176
+ session_id: str,
177
+ run_id: str | None = None,
178
+ kind: str = "agent",
179
+ ) -> dict[str, Any]:
180
+ """Grant, check and register one executor's write entitlement, or leave none."""
181
+
182
+ if kind not in RUNTIME_KINDS:
183
+ raise ControllerError("INVALID_BINDING", f"unsupported runtime kind: {kind}")
184
+ repository = inventory(repository_path)
185
+ snapshot = MetadataStore(repository.common_dir).load()
186
+ _item, record = _managed_record(repository, snapshot, target)
187
+ if (
188
+ record.task_key != task_key
189
+ or record.authority_key != authority_key
190
+ or record.owner != owner
191
+ or record.lifecycle != "ACTIVE"
192
+ ):
193
+ raise ControllerError(
194
+ "SESSION_ALLOCATION_DRIFT", "allocation no longer matches the admitted session"
195
+ )
196
+ leased = acquire_writer_lease(
197
+ repository_path,
198
+ target=target,
199
+ owner=owner,
200
+ client_id=client_id,
201
+ holder_pid=holder_pid,
202
+ session_id=session_id,
203
+ run_id=run_id,
204
+ observe=False,
205
+ )
206
+ lease = leased["decision"]
207
+ holder = lease["holder"]
208
+ try:
209
+ guarded = guard_write(
210
+ repository_path,
211
+ target=target,
212
+ task_key=task_key,
213
+ authority_key=authority_key,
214
+ owner=owner,
215
+ operation="feature-write",
216
+ holder=WriterHolder(**holder),
217
+ observe=False,
218
+ )["decision"]
219
+ if guarded.get("code") != "WRITE_ALLOWED":
220
+ raise ControllerError(
221
+ str(guarded.get("code") or "WRITE_GUARD_REFUSED"),
222
+ str(guarded.get("reason") or "write guard refused the holder"),
223
+ )
224
+ binding = runtime_binding_value(holder, lease["lease_id"], kind)
225
+ bound = update_external_binding(
226
+ repository_path,
227
+ target=target,
228
+ expected_record_version=record.record_version,
229
+ owner=owner,
230
+ binding=binding,
231
+ action="add",
232
+ observe=False,
233
+ )["decision"]
234
+ except Exception as failure:
235
+ outcome: dict[str, Any] = {"lease_id": lease["lease_id"], "lease_released": True}
236
+ try:
237
+ release_writer_lease(
238
+ repository_path,
239
+ target=target,
240
+ holder_pid=holder["pid"],
241
+ lease_id=lease["lease_id"],
242
+ holder=WriterHolder(**holder),
243
+ )
244
+ except Exception as release_failure:
245
+ # The host still knows this exact lease and releases it with its own cleanup.
246
+ outcome = {
247
+ "lease_id": lease["lease_id"],
248
+ "lease_released": False,
249
+ "lease": lease,
250
+ "release_error": getattr(release_failure, "code", type(release_failure).__name__),
251
+ }
252
+ if isinstance(failure, (ControllerError, OSError)):
253
+ failure.details = outcome # type: ignore[attr-defined]
254
+ raise
255
+ payload = envelope("session-bind")
256
+ payload["decision"] = {
257
+ "code": "SESSION_BOUND",
258
+ "reason": "write entitlement granted, checked and registered for the live executor",
259
+ "lease": lease,
260
+ "write_guard": guarded["code"],
261
+ "runtime_binding": binding,
262
+ "record_version": bound["record_version"],
263
+ "session_admission_version": SESSION_ADMISSION_VERSION,
264
+ }
265
+ return payload