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,150 @@
1
+ """Fast-forward an ACTIVE, idle canonical checkout to its published main."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .canonical_transition import _check_target_policy, _published_policy
9
+ from .controller import envelope
10
+ from .git_backend import fetch_remote_branch, git_write, inventory, is_ancestor
11
+ from .models import ControllerError
12
+ from .policy import load_policy
13
+ from .semantics import record_for as _record_for
14
+ from .store import MetadataStore
15
+ from .transaction_scope import conflicts
16
+ from .writer_lease import ProcessPathScan, own_ancestry
17
+
18
+ CANONICAL_REFRESH_VERSION = 1
19
+ _PROC_ROOT = Path("/proc")
20
+ _PROCESS_LIMIT = 5
21
+ _PATH_LIMIT = 3
22
+ _PATH_LENGTH = 512
23
+
24
+
25
+ def _process_metadata(pid: int) -> dict[str, Any]:
26
+ """Best-effort kernel identity, separate from the scan that already blocked refresh."""
27
+ unknown = {"name": "unknown", "parent_pid": None, "metadata_status": "unknown"}
28
+ try:
29
+ with (_PROC_ROOT / str(pid) / "stat").open(encoding="utf-8", errors="replace") as stream:
30
+ raw = stream.read(4097)
31
+ prefix, separator, suffix = raw.rpartition(")")
32
+ name = prefix.partition("(")[2]
33
+ if len(raw) > 4096 or not separator or not name:
34
+ return unknown
35
+ parent = int(suffix.split()[1])
36
+ if parent < 0:
37
+ return unknown
38
+ return {"name": name[:64], "parent_pid": parent, "metadata_status": "observed"}
39
+ except (OSError, ValueError, IndexError):
40
+ return unknown
41
+
42
+
43
+ class CanonicalProcessOccupancyError(ControllerError):
44
+ """The ordinary refusal with bounded advisory metadata and a guarded retry hint."""
45
+
46
+ def __init__(self, target: Path, used: list[tuple[int, list[str]]]) -> None:
47
+ shown = sorted(used)[:_PROCESS_LIMIT]
48
+ super().__init__(
49
+ "CANONICAL_IN_USE",
50
+ "live processes use the canonical checkout: "
51
+ + ", ".join(str(pid) for pid, _paths in shown),
52
+ )
53
+ processes = []
54
+ for pid, paths in shown:
55
+ metadata = _process_metadata(pid)
56
+ parent = metadata["parent_pid"]
57
+ parent_name = _process_metadata(parent)["name"] if parent is not None else "unknown"
58
+ unique_paths = list(dict.fromkeys(paths))
59
+ processes.append({
60
+ "pid": pid,
61
+ **metadata,
62
+ "parent_name": parent_name,
63
+ "matched_paths": [path[:_PATH_LENGTH] for path in unique_paths[:_PATH_LIMIT]],
64
+ "paths_truncated": len(unique_paths) > _PATH_LIMIT
65
+ or any(len(path) > _PATH_LENGTH for path in unique_paths[:_PATH_LIMIT]),
66
+ })
67
+ self.diagnostics = {
68
+ "process_count": len(used),
69
+ "processes": processes,
70
+ "processes_omitted": len(used) - len(shown),
71
+ "recovery_steps": [
72
+ "Verify process ownership and the exact owning task/session; this metadata is advisory.",
73
+ "For tools owned by a shared daemon, use its native session inventory to check active work. "
74
+ "End only the exact completed task-owned session through its normal lifecycle; "
75
+ "closing the current CLI may not release those tools.",
76
+ "After references clear, run retry_argv. If CANONICAL_REFRESH_READY is returned, "
77
+ "repeat without --dry-run. Unknown metadata keeps the original refusal.",
78
+ ],
79
+ "retry_argv": ["worktree-controller", "canonical-refresh", "--repo", str(target),
80
+ "--dry-run", "--json"],
81
+ }
82
+
83
+
84
+ def refresh_canonical(repository_path: Path, *, dry_run: bool = False) -> dict[str, Any]:
85
+ """Move canonical main forward to the fetched publication branch, or say why not.
86
+
87
+ Integrations publish from candidates, so the canonical checkout falls behind
88
+ published main. This only ever fast-forwards a clean checkout that is ACTIVE,
89
+ on the publication branch, holds no writer lease or binding and is not used by
90
+ any live process; a frozen canonical keeps the exact ``canonical-transition``
91
+ flow. Controller metadata is not changed.
92
+ """
93
+
94
+ repository_initial = inventory(repository_path)
95
+ store = MetadataStore(repository_initial.common_dir)
96
+ with store.lock(exclusive=True):
97
+ repository = inventory(repository_path)
98
+ snapshot = store._load_unlocked()
99
+ policy = load_policy(repository.canonical_path)
100
+ if not policy.present or not policy.canonical_protected or policy.policy_schema_version < 2:
101
+ raise ControllerError("INVALID_POLICY", "canonical refresh requires a governed publication branch")
102
+ item = next((worktree for worktree in repository.worktrees if worktree.canonical), None)
103
+ record = _record_for(item, snapshot) if item is not None else None
104
+ if item is None or record is None:
105
+ raise ControllerError("UNMANAGED_CANONICAL", "canonical checkout has no Controller record")
106
+ if record.lifecycle != "ACTIVE":
107
+ raise ControllerError(
108
+ "CANONICAL_NOT_ACTIVE",
109
+ f"canonical lifecycle {record.lifecycle}; a frozen canonical uses canonical-transition",
110
+ )
111
+ if item.detached or item.branch != policy.publication_branch:
112
+ raise ControllerError("CANONICAL_BRANCH", "canonical is not on the publication branch")
113
+ if not item.clean or item.locked or item.prunable:
114
+ raise ControllerError("CANONICAL_DIRTY", "canonical checkout is not clean")
115
+ if record.allocation_id in snapshot.writer_leases or record.external_bindings:
116
+ raise ControllerError("CANONICAL_IN_USE", "canonical holds a writer lease or binding")
117
+ if conflicts(store, allocation=record.allocation_id, target=item.path, assume_serialized=True):
118
+ raise ControllerError("INTERRUPTED_TRANSACTION", "canonical refresh conflicts with a transaction")
119
+ ancestry = own_ancestry()
120
+ used = [entry for entry in ProcessPathScan().referenced_paths_in_use(item.path)
121
+ if entry[0] not in ancestry]
122
+ if used:
123
+ raise CanonicalProcessOccupancyError(item.path, used)
124
+ before = item.head
125
+ target_oid = fetch_remote_branch(item.path, policy.publication_remote, policy.publication_branch)
126
+ decision: dict[str, Any] = {
127
+ "path": str(item.path),
128
+ "branch": item.branch,
129
+ "before": before,
130
+ "target": target_oid,
131
+ "dry_run": dry_run,
132
+ }
133
+ if before == target_oid:
134
+ decision.update(code="CANONICAL_CURRENT", reason="canonical already equals published main")
135
+ elif not is_ancestor(item.path, before, target_oid):
136
+ raise ControllerError("NON_FAST_FORWARD", "canonical main is not an ancestor of published main")
137
+ elif dry_run:
138
+ decision.update(code="CANONICAL_REFRESH_READY", reason="canonical can fast-forward")
139
+ else:
140
+ # A published main that this Controller could not govern is refused first.
141
+ _check_target_policy(_published_policy(repository, target_oid, policy),
142
+ policy.publication_branch, policy.publication_remote)
143
+ git_write(item.path, ("merge", "--ff-only", "--no-edit", "--no-overwrite-ignore", target_oid))
144
+ after = next(worktree for worktree in inventory(item.path).worktrees if worktree.canonical)
145
+ if after.head != target_oid or not after.clean or after.branch != policy.publication_branch:
146
+ raise ControllerError("REFRESH_INCOMPLETE", "canonical did not reach exact published main")
147
+ decision.update(code="CANONICAL_REFRESHED", reason="canonical fast-forwarded to published main")
148
+ payload = envelope("canonical-refresh")
149
+ payload["decision"] = decision
150
+ return payload
@@ -0,0 +1,324 @@
1
+ """Move a frozen canonical checkout onto an exact published main commit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from dataclasses import replace
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .controller import status
11
+ from .git_backend import (
12
+ _git,
13
+ fetch_remote_branch,
14
+ git_write,
15
+ head_paths,
16
+ inventory,
17
+ is_ancestor,
18
+ resolve_commit,
19
+ update_branch_ref,
20
+ validate_branch_name,
21
+ )
22
+ from .lifecycle import _updated_record
23
+ from .models import (
24
+ ControllerError,
25
+ MetadataRecord,
26
+ MetadataSnapshot,
27
+ RepositoryInventory,
28
+ WorktreeGitState,
29
+ WorktreePolicy,
30
+ )
31
+ from .policy import default_policy, load_policy, parse_policy
32
+ from .semantics import managed_record as _managed_record
33
+ from .store import MetadataStore
34
+ from .transaction_scope import conflicts
35
+
36
+
37
+ def _check_frozen(
38
+ repository: RepositoryInventory,
39
+ item: WorktreeGitState,
40
+ record: MetadataRecord,
41
+ snapshot: MetadataSnapshot,
42
+ *,
43
+ expected_branch: str,
44
+ expected_head: str,
45
+ owner: str,
46
+ target_branch: str,
47
+ target_oid: str,
48
+ expected_main_head: str,
49
+ ) -> None:
50
+ if not item.canonical or item.path != repository.canonical_path:
51
+ raise ControllerError("BLOCK_CANONICAL", "transition requires the canonical checkout")
52
+ if record.owner != owner:
53
+ raise ControllerError("OWNER_MISMATCH", "frozen canonical owner differs")
54
+ if record.lifecycle != "HANDOFF_READY" or record.role not in {"canonical", "control-plane"}:
55
+ raise ControllerError("INVALID_LIFECYCLE", "canonical transition requires a frozen handoff")
56
+ if not record.finalizer_record_id or any(
57
+ value != expected_head
58
+ for value in (record.handoff_commit, record.task_commit, record.finalizer_commit)
59
+ ):
60
+ raise ControllerError("HANDOFF_IDENTITY_MISMATCH", "frozen handoff evidence differs")
61
+ if item.detached or item.locked or item.prunable or not item.clean:
62
+ raise ControllerError("DIRTY_WORKTREE", "canonical checkout is not clean and attached")
63
+ refresh_main = expected_branch == target_branch
64
+ starting_head = expected_main_head if refresh_main else expected_head
65
+ if (item.branch, item.head) not in {
66
+ (expected_branch, starting_head),
67
+ (target_branch, target_oid),
68
+ }:
69
+ raise ControllerError("HEAD_MISMATCH", "canonical branch and HEAD disagree")
70
+ retained_head = resolve_commit(item.path, f"refs/heads/{expected_branch}")
71
+ if retained_head not in ({expected_main_head, target_oid} if refresh_main else {expected_head}):
72
+ raise ControllerError("HEAD_MISMATCH", "frozen branch no longer retains its handoff")
73
+ if record.external_bindings or any(
74
+ resource["state"] != "cleaned" for resource in record.disposable_resources
75
+ ):
76
+ raise ControllerError("ACTIVE_BINDING", "canonical transition has a resource binding")
77
+ if record.source_allocations or record.quarantine_expected_commit or record.recovery_ref:
78
+ raise ControllerError(
79
+ "ACTIVE_LINEAGE", "canonical transition cannot discard recovery lineage"
80
+ )
81
+ if snapshot.publication_lease is not None or record.allocation_id in snapshot.writer_leases:
82
+ raise ControllerError("ACTIVE_LEASE", "canonical transition conflicts with an active lease")
83
+ if any(
84
+ record.allocation_id in intent.source_allocations
85
+ and intent.state not in {"RELEASED", "ABANDONED"}
86
+ for intent in snapshot.integration_intents.values()
87
+ ) or any(
88
+ intent.allocation_id == record.allocation_id for intent in snapshot.release_intents.values()
89
+ ):
90
+ raise ControllerError(
91
+ "ACTIVE_INTENT", "canonical transition conflicts with an active intent"
92
+ )
93
+ if any(
94
+ other.path != item.path and other.branch == target_branch for other in repository.worktrees
95
+ ):
96
+ raise ControllerError("BRANCH_OCCUPIED", "target main branch is checked out elsewhere")
97
+
98
+
99
+ def _published_policy(
100
+ repository: RepositoryInventory, target_oid: str, current: WorktreePolicy
101
+ ) -> WorktreePolicy:
102
+ relative = ".agents/worktree-policy.toml"
103
+ entries = _git(
104
+ repository.canonical_path, "ls-tree", "-z", target_oid, "--", relative
105
+ ).stdout.split(b"\0")
106
+ entries = [entry for entry in entries if entry]
107
+ if not entries:
108
+ parent = _git(repository.canonical_path, "ls-tree", target_oid, "--", ".agents").stdout
109
+ if parent and not parent.startswith(b"040000 tree "):
110
+ raise ControllerError("INVALID_POLICY", "published policy parent is not a directory")
111
+ # An untracked local policy survives a fast-forward; a tracked deletion does not.
112
+ return (
113
+ default_policy(repository.canonical_path)
114
+ if head_paths(repository.canonical_path, relative)
115
+ else current
116
+ )
117
+ header, separator, name = entries[0].partition(b"\t")
118
+ fields = header.split()
119
+ if (
120
+ len(entries) != 1
121
+ or not separator
122
+ or name != relative.encode()
123
+ or len(fields) != 3
124
+ or fields[0] not in {b"100644", b"100755"}
125
+ or fields[1] != b"blob"
126
+ ):
127
+ raise ControllerError("INVALID_POLICY", "published policy is not a regular file")
128
+ raw = _git(repository.canonical_path, "cat-file", "blob", fields[2].decode()).stdout
129
+ try:
130
+ data = tomllib.loads(raw.decode("utf-8"))
131
+ except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc:
132
+ raise ControllerError("INVALID_POLICY", f"cannot read published policy: {exc}") from exc
133
+ return parse_policy(repository.canonical_path, data)
134
+
135
+
136
+ def _check_target_policy(policy: WorktreePolicy, branch: str, remote: str) -> None:
137
+ if (
138
+ not policy.present
139
+ or not policy.canonical_protected
140
+ or policy.publication_branch != branch
141
+ or policy.publication_remote != remote
142
+ or "control-plane" not in policy.canonical_allowed_roles
143
+ ):
144
+ raise ControllerError("INVALID_TARGET_POLICY", "published main has incompatible policy")
145
+
146
+
147
+ def transition_canonical(
148
+ repository_path: Path,
149
+ *,
150
+ target: Path,
151
+ expected_record_version: int,
152
+ owner: str,
153
+ expected_branch: str,
154
+ expected_head: str,
155
+ expected_main_head: str,
156
+ expected_remote_head: str,
157
+ new_owner: str,
158
+ new_task_key: str,
159
+ new_authority_key: str,
160
+ ) -> dict[str, Any]:
161
+ """Fast-forward local main and rebind canonical authority without releasing its path.
162
+
163
+ A retry can finish a checkout that reached the exact target before metadata was
164
+ written. A frozen publication branch is refreshed in place and retains its
165
+ handoff evidence; other frozen branches retain both their ref and evidence.
166
+ """
167
+
168
+ if not all(
169
+ value.strip() and "\x00" not in value
170
+ for value in (owner, new_owner, new_task_key, new_authority_key)
171
+ ):
172
+ raise ControllerError("INVALID_IDENTITY", "transition identities must be nonempty")
173
+ repository_initial = inventory(repository_path)
174
+ store = MetadataStore(repository_initial.common_dir)
175
+ with store.lock(exclusive=True):
176
+ repository = inventory(repository_path)
177
+ snapshot = store._load_unlocked()
178
+ item, record = _managed_record(repository, snapshot, target)
179
+ if record.record_version != expected_record_version:
180
+ raise ControllerError("CAS_MISMATCH", "canonical record version changed")
181
+ if any(
182
+ other.allocation_id != record.allocation_id
183
+ and other.lifecycle not in {"RELEASED", "ARCHIVED"}
184
+ and (other.task_key == new_task_key or other.authority_key == new_authority_key)
185
+ for other in snapshot.records.values()
186
+ ):
187
+ raise ControllerError(
188
+ "AUTHORITY_CONFLICT", "new control-plane identity is already active"
189
+ )
190
+ policy = load_policy(repository.canonical_path)
191
+ if (
192
+ not policy.present
193
+ or not policy.canonical_protected
194
+ or policy.publication_branch not in {"main", "master"}
195
+ ):
196
+ raise ControllerError(
197
+ "INVALID_POLICY", "canonical transition requires governed main or master"
198
+ )
199
+ remote = policy.publication_remote
200
+ main_branch = policy.publication_branch
201
+ validate_branch_name(item.path, expected_branch)
202
+ refresh_main = expected_branch == main_branch
203
+ if len(expected_remote_head) != 40 or any(
204
+ character not in "0123456789abcdef" for character in expected_remote_head
205
+ ):
206
+ raise ControllerError(
207
+ "INVALID_COMMIT", "expected remote HEAD must be a full commit OID"
208
+ )
209
+ source_oid = resolve_commit(item.path, expected_head)
210
+ original_main = resolve_commit(item.path, expected_main_head)
211
+ if (source_oid, original_main) != (expected_head, expected_main_head):
212
+ raise ControllerError("INVALID_COMMIT", "transition requires full commit OIDs")
213
+ _check_frozen(
214
+ repository,
215
+ item,
216
+ record,
217
+ snapshot,
218
+ expected_branch=expected_branch,
219
+ expected_head=expected_head,
220
+ owner=owner,
221
+ target_branch=main_branch,
222
+ target_oid=expected_remote_head,
223
+ expected_main_head=original_main,
224
+ )
225
+ if conflicts(
226
+ store, allocation=record.allocation_id, target=item.path, assume_serialized=True
227
+ ):
228
+ raise ControllerError(
229
+ "INTERRUPTED_TRANSACTION", "canonical transition conflicts with a transaction"
230
+ )
231
+ target_oid = fetch_remote_branch(item.path, remote, main_branch)
232
+ if target_oid != expected_remote_head:
233
+ raise ControllerError(
234
+ "REMOTE_DRIFT", "remote main differs from the exact expected commit"
235
+ )
236
+ if (
237
+ not is_ancestor(item.path, source_oid, target_oid)
238
+ or not is_ancestor(item.path, original_main, target_oid)
239
+ or (refresh_main and not is_ancestor(item.path, source_oid, original_main))
240
+ ):
241
+ raise ControllerError(
242
+ "NON_FAST_FORWARD", "remote main does not contain frozen and local main commits"
243
+ )
244
+ local_main = resolve_commit(item.path, f"refs/heads/{main_branch}")
245
+ if local_main not in {original_main, target_oid}:
246
+ raise ControllerError("MAIN_DRIFT", "local main changed outside transition bounds")
247
+ if refresh_main:
248
+ # Validate the immutable target before moving a checked-out publication ref.
249
+ _check_target_policy(_published_policy(repository, target_oid, policy), main_branch, remote)
250
+ if local_main != target_oid:
251
+ git_write(
252
+ item.path, ("merge", "--ff-only", "--no-edit", "--no-overwrite-ignore", target_oid)
253
+ )
254
+ else:
255
+ if local_main == original_main and original_main != target_oid:
256
+ update_branch_ref(item.path, main_branch, expected=original_main, new=target_oid)
257
+ if item.branch == expected_branch:
258
+ git_write(item.path, ("checkout", "--no-overwrite-ignore", main_branch))
259
+ try:
260
+ after = inventory(item.path)
261
+ current = next(worktree for worktree in after.worktrees if worktree.canonical)
262
+ if current.branch != main_branch or current.head != target_oid or not current.clean:
263
+ raise ControllerError(
264
+ "TRANSITION_INCOMPLETE", "canonical checkout did not reach exact main"
265
+ )
266
+ target_policy = load_policy(after.canonical_path)
267
+ _check_target_policy(target_policy, main_branch, remote)
268
+ if fetch_remote_branch(item.path, remote, main_branch) != target_oid:
269
+ raise ControllerError(
270
+ "REMOTE_DRIFT", "remote main moved during canonical transition"
271
+ )
272
+ except ControllerError:
273
+ # An in-place advance stays frozen for exact recovery; never rewind main.
274
+ if not refresh_main and item.branch == expected_branch:
275
+ git_write(item.path, ("checkout", "--no-overwrite-ignore", expected_branch))
276
+ raise
277
+ updated = _updated_record(
278
+ record,
279
+ "CANONICAL_MAIN_REFRESHED" if refresh_main else "CANONICAL_TRANSITIONED",
280
+ lifecycle="ACTIVE",
281
+ owner=new_owner,
282
+ task_key=new_task_key,
283
+ authority_key=new_authority_key,
284
+ module="repository",
285
+ role="control-plane",
286
+ base_commit=target_oid,
287
+ task_commit=target_oid,
288
+ handoff_commit=None,
289
+ finalizer_commit=None,
290
+ finalizer_record_id=None,
291
+ scope_paths=(),
292
+ source_allocations=(),
293
+ )
294
+ event = {
295
+ **updated.history[-1],
296
+ "old_branch": expected_branch,
297
+ "old_head": source_oid,
298
+ "old_owner": owner,
299
+ "old_task_key": record.task_key,
300
+ "old_authority_key": record.authority_key,
301
+ "old_finalizer_record_id": record.finalizer_record_id,
302
+ "old_scope_paths": list(record.scope_paths),
303
+ "main_before": original_main,
304
+ "main_after": target_oid,
305
+ "remote": remote,
306
+ }
307
+ updated = replace(updated, history=(*updated.history[:-1], event))
308
+ store.replace_record(updated)
309
+ payload = status(repository_path)
310
+ payload["command"] = "canonical-transition"
311
+ payload["decision"] = {
312
+ "code": "CANONICAL_MAIN_REFRESHED" if refresh_main else "CANONICAL_TRANSITIONED",
313
+ "allocation_id": updated.allocation_id,
314
+ "record_version": updated.record_version,
315
+ "old_branch": expected_branch,
316
+ "old_head": source_oid,
317
+ "main_head": target_oid,
318
+ "reason": (
319
+ "exact remote main verified; frozen handoff history retained and canonical authority moved"
320
+ if refresh_main else
321
+ "exact remote main verified; frozen branch retained and canonical authority moved"
322
+ ),
323
+ }
324
+ return payload