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,1974 @@
1
+ """Controller decisions over Git inventory and repository-local metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ from collections import defaultdict
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .git_backend import commit_tree_oid, git_state_fingerprint, inventory, resolve_commit
13
+ from .lifecycle_observation import observe_repository_orphans, observe_worktree
14
+ from .models import (
15
+ ACTIVE_AUTHORITY_STATES,
16
+ CANONICAL_ROLES,
17
+ EMERGENCY_ROLES,
18
+ FEATURE_AUTHORITY_ROLES,
19
+ LIFECYCLE_STATES,
20
+ ORDINARY_ROLES,
21
+ RECOVERY_ROLES,
22
+ SCHEMA_VERSION,
23
+ WORKTREE_ROLES,
24
+ ControllerError,
25
+ MetadataRecord,
26
+ MetadataSnapshot,
27
+ RepositoryInventory,
28
+ WorktreeGitState,
29
+ WorktreePolicy,
30
+ WriterHolder,
31
+ )
32
+ from .mount_diagnostics import diagnose_mounts, mount_warnings
33
+ from .policy import load_policy
34
+ from .semantics import (
35
+ EVIDENCE_LIFECYCLES,
36
+ evidence_head_issues,
37
+ integration_candidate_issues,
38
+ integration_intent_abandoned,
39
+ integration_queue_order,
40
+ normalize_scope_paths,
41
+ path_scope_issues,
42
+ publication_ref,
43
+ unallocated_integration_conflict,
44
+ )
45
+ from .semantics import record_for as _record_for
46
+ from .store import TIMEZONE, MetadataStore, new_record
47
+ from .writer_lease import check_writer_entitlement
48
+
49
+ WRITE_OPERATIONS = frozenset({"feature-write", "integration-write", "recovery", "finalization"})
50
+
51
+
52
+ def envelope(command: str) -> dict[str, Any]:
53
+ from . import __version__
54
+
55
+ return {
56
+ "tool_version": __version__,
57
+ "schema_version": SCHEMA_VERSION,
58
+ "command": command,
59
+ "repository": None,
60
+ "decision": None,
61
+ "worktrees": [],
62
+ "warnings": [],
63
+ "errors": [],
64
+ }
65
+
66
+
67
+ def _warning(code: str, message: str, **details: str) -> dict[str, str]:
68
+ return {"code": code, "message": message, **details}
69
+
70
+
71
+ def _retirement_execution_findings(
72
+ store: MetadataStore,
73
+ ) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
74
+ """Outstanding retirement grants and removals nobody can attest.
75
+
76
+ A grant is live work in someone else's hands, so it is reported without being
77
+ called a failure. An abandoned removal is different: the ref is gone and the
78
+ Controller never observed the authorized transition, so it must stay visible
79
+ instead of leaving ``branch-retirement-status`` at PARTIAL with no explanation.
80
+ """
81
+
82
+ directory = store.branch_retirement_authorizations_dir
83
+ if not directory.is_dir():
84
+ return [], []
85
+ outstanding: list[dict[str, str]] = []
86
+ unattested: list[dict[str, str]] = []
87
+ for path in sorted(directory.iterdir()):
88
+ if not path.name.endswith(".json") or not path.is_file():
89
+ continue
90
+ try:
91
+ value = dict(store._read_json(path))
92
+ except ControllerError:
93
+ unattested.append(
94
+ {"path": path.name, "code": "INVALID_RETIREMENT_EXECUTION_RECORD"}
95
+ )
96
+ continue
97
+ if path.name.endswith("-abandoned.json"):
98
+ if value.get("classification") == "ABANDONED_UNATTESTED_REMOVAL":
99
+ unattested.append(
100
+ {
101
+ "authorization_id": str(value.get("authorization_id", path.stem)),
102
+ "plan_id": str(value.get("plan_id", "")),
103
+ "operation": str(value.get("operation", "")),
104
+ "ref": str(value.get("ref", "")),
105
+ "code": "RETIREMENT_REMOVAL_UNATTESTED",
106
+ }
107
+ )
108
+ continue
109
+ outstanding.append(
110
+ {
111
+ "authorization_id": str(value.get("authorization_id", path.stem)),
112
+ "plan_id": str(value.get("plan_id", "")),
113
+ "operation": str(value.get("operation", "")),
114
+ "branch": str(value.get("branch", "")),
115
+ "ref": str(value.get("ref", "")),
116
+ "expires_at": str(value.get("expires_at", "")),
117
+ }
118
+ )
119
+ return outstanding, unattested
120
+
121
+
122
+ def _lock_warnings(store: MetadataStore) -> list[dict[str, str]]:
123
+ """The repository lock arbitrates every writer, so its shape is authority."""
124
+
125
+ def _populated(directory: Path) -> bool:
126
+ try:
127
+ return next(directory.iterdir(), None) is not None
128
+ except OSError:
129
+ return False
130
+
131
+ if not os.path.lexists(store.lock_file):
132
+ if store.repo_file.exists() or any(
133
+ directory.is_dir() and _populated(directory)
134
+ for directory in (store.records_dir, store.transactions_dir)
135
+ ):
136
+ return [
137
+ _warning(
138
+ "REPOSITORY_LOCK_MISSING",
139
+ "repository lock file is missing; mutations fail closed until a bootstrap "
140
+ "command recreates it",
141
+ path=str(store.lock_file),
142
+ )
143
+ ]
144
+ return []
145
+ try:
146
+ lock_shape_valid = stat.S_ISREG(os.lstat(store.lock_file).st_mode)
147
+ except OSError:
148
+ return [] # Environment diagnosis owns inaccessible paths.
149
+ if not lock_shape_valid:
150
+ return [
151
+ _warning(
152
+ "REPOSITORY_LOCK_INVALID",
153
+ "repository lock is not a regular file; controller authority requires review",
154
+ path=str(store.lock_file),
155
+ )
156
+ ]
157
+ return []
158
+
159
+
160
+ def _environment_first(error: ControllerError, diagnosis: dict[str, Any]) -> ControllerError:
161
+ """Keep access failures ahead of lock authority until the environment is known good."""
162
+ code = diagnosis["code"]
163
+ if code != "HEALTHY" and error.code in {
164
+ "REPOSITORY_LOCK_INVALID",
165
+ "REPOSITORY_LOCK_ACCESS_UNKNOWN",
166
+ "MISSING_REPOSITORY_LOCK",
167
+ "FILESYSTEM_PERMISSION",
168
+ }:
169
+ facts = ", ".join(diagnosis["facts"]) or "access could not be confirmed"
170
+ return ControllerError(code, f"repository environment diagnosis: {facts}")
171
+ if error.code == "REPOSITORY_LOCK_ACCESS_UNKNOWN":
172
+ return ControllerError("UNKNOWN", str(error))
173
+ return error
174
+
175
+
176
+ def _environment_warnings(diagnostics: dict[str, Any]) -> list[dict[str, str]]:
177
+ warnings = mount_warnings(diagnostics)
178
+ code = diagnostics["environment"]["code"]
179
+ if code != "HEALTHY":
180
+ warnings = [row for row in warnings if row["code"] != "REPOSITORY_LOCK_SHAPE_INVALID"]
181
+ if code == "UNKNOWN":
182
+ warnings.append(
183
+ _warning(
184
+ "ENVIRONMENT_UNKNOWN",
185
+ "repository mount or namespace access could not be confirmed",
186
+ severity="degraded",
187
+ )
188
+ )
189
+ elif code == "FILESYSTEM_PERMISSION":
190
+ warnings.append(
191
+ _warning(
192
+ "FILESYSTEM_PERMISSION",
193
+ "repository filesystem access is restricted",
194
+ severity="blocking",
195
+ )
196
+ )
197
+ return warnings
198
+
199
+
200
+ def _primary_diagnosis(
201
+ environment: dict[str, Any], findings: list[dict[str, str]], *, failed: bool
202
+ ) -> str:
203
+ if environment["code"] != "HEALTHY":
204
+ return str(environment["code"])
205
+ codes = {row["code"] for row in findings}
206
+ if "FILESYSTEM_PERMISSION" in codes:
207
+ return "FILESYSTEM_PERMISSION"
208
+ if "UNKNOWN" in codes:
209
+ return "UNKNOWN"
210
+ if codes & {
211
+ "REPOSITORY_LOCK_INVALID",
212
+ "REPOSITORY_LOCK_SHAPE_INVALID",
213
+ "REPOSITORY_LOCK_MISSING",
214
+ "MISSING_REPOSITORY_LOCK",
215
+ }:
216
+ return "CONTROLLER_LOCK_AUTHORITY"
217
+ return "CONTROLLER_METADATA" if failed else "NONE"
218
+
219
+
220
+ def _observed_worktrees(
221
+ repository: RepositoryInventory,
222
+ policy: WorktreePolicy,
223
+ snapshot: MetadataSnapshot,
224
+ *,
225
+ namespace_healthy: bool,
226
+ ) -> list[dict[str, Any]]:
227
+ worktrees = []
228
+ for item in repository.worktrees:
229
+ record = _record_for(item, snapshot)
230
+ row = _worktree_payload(item, record, snapshot)
231
+ row["lifecycle_observation"] = observe_worktree(
232
+ item, record, snapshot, policy, namespace_healthy=namespace_healthy
233
+ )
234
+ worktrees.append(row)
235
+ return worktrees
236
+
237
+
238
+ def _intent_for_allocation(snapshot: MetadataSnapshot, allocation_id: str | None) -> Any | None:
239
+ if allocation_id is None:
240
+ return None
241
+ matches = [
242
+ intent
243
+ for intent in snapshot.integration_intents.values()
244
+ if intent.allocation_id == allocation_id
245
+ ]
246
+ if len(matches) > 1:
247
+ raise ControllerError("INVALID_METADATA", "multiple intents bind one allocation")
248
+ return matches[0] if matches else None
249
+
250
+
251
+ def _effective_intent_state(snapshot: MetadataSnapshot, intent: Any) -> str:
252
+ reconciliations = [
253
+ receipt
254
+ for receipt in snapshot.integration_reconciliation_receipts.values()
255
+ if receipt.intent_id == intent.intent_id
256
+ and receipt.result == "STALE_INTENT_TERMINAL_ACKNOWLEDGED"
257
+ ]
258
+ if len(reconciliations) > 1:
259
+ raise ControllerError(
260
+ "INVALID_METADATA", "integration intent has multiple terminal reconciliations"
261
+ )
262
+ if reconciliations:
263
+ return reconciliations[0].effective_terminal_state
264
+ return "ABANDONED" if integration_intent_abandoned(intent) else intent.state
265
+
266
+
267
+ def _integration_intent_order(intent: Any) -> tuple[int, int | str, str]:
268
+ if intent.queue_sequence > 0:
269
+ return (1, intent.queue_sequence, intent.intent_id)
270
+ return (0, intent.enqueued_at, intent.intent_id)
271
+
272
+
273
+ def _release_intent_for_allocation(
274
+ snapshot: MetadataSnapshot, allocation_id: str | None
275
+ ) -> Any | None:
276
+ if allocation_id is None:
277
+ return None
278
+ matches = sorted(
279
+ (
280
+ intent
281
+ for intent in snapshot.release_intents.values()
282
+ if intent.allocation_id == allocation_id
283
+ ),
284
+ key=lambda intent: (intent.updated_at, intent.intent_id),
285
+ reverse=True,
286
+ )
287
+ active = [intent for intent in matches if intent.status in {"PENDING", "BLOCKED_RETRYABLE"}]
288
+ if len(active) > 1:
289
+ raise ControllerError("INVALID_METADATA", "multiple active release intents bind allocation")
290
+ return active[0] if active else matches[0] if matches else None
291
+
292
+
293
+ def capacity_accounting(
294
+ repository: RepositoryInventory,
295
+ policy: WorktreePolicy,
296
+ snapshot: MetadataSnapshot,
297
+ ) -> dict[str, Any]:
298
+ """Report inventory and declared ownership/occupancy without probing processes.
299
+
300
+ Resource admission treats the legacy checkout/role limits as statistics. A
301
+ writer lease is ownership evidence, including when its holder has died; only
302
+ an existing runtime binding declares execution occupancy until formal clear.
303
+ Neither count proves process liveness or business completion.
304
+ """
305
+
306
+ registered = [
307
+ (item, record)
308
+ for item in repository.worktrees
309
+ if (record := _record_for(item, snapshot)) is not None
310
+ ]
311
+ managed = [(item, record) for item, record in registered if not item.canonical]
312
+ ordinary = [(item, record) for item, record in managed if record.role in ORDINARY_ROLES]
313
+ writers = [record for _item, record in ordinary if record.lifecycle != "RETAINED"]
314
+ retained = [record for _item, record in ordinary if record.lifecycle == "RETAINED"]
315
+ leased = set(snapshot.writer_leases)
316
+ runtime_occupied = sum(
317
+ any(binding.startswith("runtime:") for binding in record.external_bindings)
318
+ for _item, record in registered
319
+ )
320
+ cleanup_pending = {
321
+ intent.allocation_id
322
+ for intent in snapshot.release_intents.values()
323
+ if intent.status in {"PENDING", "BLOCKED_RETRYABLE"}
324
+ }
325
+ return {
326
+ "physical_used": len(repository.worktrees),
327
+ "physical_limit": policy.hard_limit,
328
+ "normal_limit": policy.normal_limit,
329
+ "ordinary_physical": len(ordinary),
330
+ "ordinary_limit": policy.feature_audit_slots + policy.retained_slots,
331
+ "writers_used": len(writers),
332
+ "writer_limit": policy.feature_audit_slots,
333
+ "retained_used": len(retained),
334
+ "integration_used": sum(1 for _item, record in managed if record.role == "integration"),
335
+ "integration_limit": policy.integration_candidate_limit,
336
+ "emergency_used": sum(1 for _item, record in managed if record.role in EMERGENCY_ROLES),
337
+ "emergency_limit": policy.emergency_slots,
338
+ "recovery_used": sum(1 for _item, record in managed if record.role in RECOVERY_ROLES),
339
+ "recovery_limit": policy.recovery_reserve,
340
+ # Legacy allocation statistics do not grant or reject resource admission.
341
+ "legacy_reserved": sum(1 for record in writers if record.allocation_id not in leased),
342
+ "writer_leases_recorded": sum(record.allocation_id in leased for _item, record in registered),
343
+ "runtime_occupied": runtime_occupied,
344
+ "live_executions": runtime_occupied,
345
+ "live_executions_semantics": "runtime-binding occupancy; process liveness is not observed",
346
+ "runtime_bound": sum(1 for _item, record in managed if record.external_bindings),
347
+ "cleanup_pending": sum(
348
+ 1 for _item, record in managed if record.allocation_id in cleanup_pending
349
+ ),
350
+ }
351
+
352
+
353
+ def governance_capacity(
354
+ repository: RepositoryInventory,
355
+ policy: WorktreePolicy,
356
+ snapshot: MetadataSnapshot,
357
+ ) -> dict[str, Any]:
358
+ """Observe declared checkout limits without changing resource admission."""
359
+ ordinary = [
360
+ record
361
+ for item in repository.worktrees
362
+ if not item.canonical
363
+ and (record := _record_for(item, snapshot)) is not None
364
+ and record.role in ORDINARY_ROLES
365
+ ]
366
+ used = {
367
+ "active": sum(record.lifecycle == "ACTIVE" for record in ordinary),
368
+ "retained": sum(record.lifecycle == "RETAINED" for record in ordinary),
369
+ "total": len(repository.worktrees),
370
+ }
371
+ limits = {
372
+ "active_limit": policy.feature_audit_slots,
373
+ "retained_limit": policy.retained_slots,
374
+ "total_limit": policy.hard_limit,
375
+ }
376
+ return {
377
+ "mode": "observe",
378
+ "scope": "active/retained: feature and audit linked worktrees; total: all Git registered worktrees",
379
+ "used": used,
380
+ "limits": limits,
381
+ "over_limit": {
382
+ name: used[name] > limits[f"{name}_limit"] for name in used
383
+ },
384
+ }
385
+
386
+
387
+ def _repository_payload(
388
+ repository: RepositoryInventory,
389
+ policy: WorktreePolicy,
390
+ snapshot: MetadataSnapshot,
391
+ ) -> dict[str, Any]:
392
+ count = len(repository.worktrees)
393
+ retained = sum(
394
+ 1
395
+ for item in repository.worktrees
396
+ if (record := _record_for(item, snapshot)) is not None and record.lifecycle == "RETAINED"
397
+ )
398
+ accounting = capacity_accounting(repository, policy, snapshot)
399
+ queued = sorted(
400
+ (
401
+ intent
402
+ for intent in snapshot.integration_intents.values()
403
+ if _effective_intent_state(snapshot, intent) in {"QUEUED", "WAITING_SCOPE"}
404
+ ),
405
+ key=lambda value: integration_queue_order(snapshot, value),
406
+ )
407
+ return {
408
+ "repository_id": snapshot.repository_id,
409
+ "canonical_path": str(repository.canonical_path),
410
+ "common_dir": str(repository.common_dir),
411
+ "requested_path": str(repository.requested_path),
412
+ "worktree_count": count,
413
+ "governance_capacity": governance_capacity(repository, policy, snapshot),
414
+ "admission_strategy": "resource-budget-v1" if policy.resource_admission else "legacy-slots",
415
+ "checkout_limits_enforced": not policy.resource_admission,
416
+ "normal_limit": policy.normal_limit,
417
+ "hard_limit": policy.hard_limit,
418
+ "normal_slots_remaining": max(policy.normal_limit - (count - retained), 0),
419
+ "hard_slots_remaining": max(policy.hard_limit - count, 0),
420
+ **accounting,
421
+ # Retained work is bounded by the ordinary pool it shares, so the remaining
422
+ # room is the pool's, not a separate quota's.
423
+ "retained_limit": accounting["ordinary_limit"],
424
+ "retained_slots_remaining": max(
425
+ accounting["ordinary_limit"] - accounting["ordinary_physical"], 0
426
+ ),
427
+ "ordinary_slots_remaining": max(
428
+ accounting["ordinary_limit"] - accounting["ordinary_physical"], 0
429
+ ),
430
+ "writer_slots_remaining": max(accounting["writer_limit"] - accounting["writers_used"], 0),
431
+ "integration_candidate_limit": policy.integration_candidate_limit,
432
+ "integration_prepare_limit": policy.integration_prepare_limit,
433
+ "main_publish_limit": policy.main_publish_limit,
434
+ "recovery_reserve": policy.recovery_reserve,
435
+ "feature_or_audit_limit": policy.feature_audit_slots,
436
+ "emergency_limit": policy.emergency_slots,
437
+ "integration_queue_depth": len(queued),
438
+ "oldest_integration_enqueued_at": queued[0].enqueued_at if queued else None,
439
+ "publication_remote": policy.publication_remote,
440
+ "publication_branch": policy.publication_branch,
441
+ "automatic_feature_audit_release": policy.automatic_feature_audit_release,
442
+ "automatic_integration_continuation": policy.automatic_integration_continuation,
443
+ "preserved_auto_release": policy.preserved_auto_release,
444
+ "release_intent_ttl_seconds": policy.release_intent_ttl_seconds,
445
+ "active_release_intent_count": sum(
446
+ intent.status in {"PENDING", "BLOCKED_RETRYABLE"}
447
+ for intent in snapshot.release_intents.values()
448
+ ),
449
+ "publication_lease": (
450
+ {
451
+ "lease_id": snapshot.publication_lease.lease_id,
452
+ "repository_id": snapshot.publication_lease.repository_id,
453
+ "allocation_id": snapshot.publication_lease.allocation_id,
454
+ "owner": snapshot.publication_lease.holder,
455
+ "holder": snapshot.publication_lease.holder,
456
+ "run_id": snapshot.publication_lease.run_id,
457
+ "remote": snapshot.publication_lease.remote,
458
+ "branch": snapshot.publication_lease.branch,
459
+ "target_ref": snapshot.publication_lease.target_ref,
460
+ "candidate_head": snapshot.publication_lease.candidate_oid,
461
+ "candidate_oid": snapshot.publication_lease.candidate_oid,
462
+ "remote_head": snapshot.publication_lease.expected_main_oid,
463
+ "expected_main_oid": snapshot.publication_lease.expected_main_oid,
464
+ "finalizer_record_id": snapshot.publication_lease.validation_evidence_id,
465
+ "validation_evidence_id": snapshot.publication_lease.validation_evidence_id,
466
+ "acquired_at": snapshot.publication_lease.acquired_at,
467
+ "expires_at": snapshot.publication_lease.expires_at,
468
+ }
469
+ if snapshot.publication_lease
470
+ else None
471
+ ),
472
+ "policy_path": str(policy.path),
473
+ "policy_present": policy.present,
474
+ "metadata_path": str(MetadataStore(repository.common_dir).root),
475
+ }
476
+
477
+
478
+ def _worktree_payload(
479
+ item: WorktreeGitState,
480
+ record: MetadataRecord | None,
481
+ snapshot: MetadataSnapshot | None = None,
482
+ ) -> dict[str, Any]:
483
+ from .owner_actions import owner_action
484
+
485
+ action = None
486
+ if record is not None and not item.clean and not item.canonical:
487
+ action = owner_action(str(item.path), {
488
+ "path": str(item.path), "head": item.head, "branch": item.branch,
489
+ **{key: getattr(record, key) for key in
490
+ ("allocation_id", "task_key", "authority_key", "owner", "record_version")},
491
+ }, reason="DIRTY_WORKTREE", required_actions=(
492
+ "review_task_files", "finalize_or_checkpoint", "preserve_unknown_files", "park_or_handoff"))
493
+ intent = (
494
+ _intent_for_allocation(snapshot, record.allocation_id)
495
+ if snapshot is not None and record is not None
496
+ else None
497
+ )
498
+ release_intent = (
499
+ _release_intent_for_allocation(snapshot, record.allocation_id)
500
+ if snapshot is not None and record is not None
501
+ else None
502
+ )
503
+ return {
504
+ "kind": "canonical" if item.canonical else "secondary",
505
+ "canonical": item.canonical,
506
+ "path": str(item.path),
507
+ "branch": item.branch,
508
+ "detached": item.detached,
509
+ "head": item.head,
510
+ "clean": item.clean,
511
+ "dirty": not item.clean,
512
+ "changes": {
513
+ "staged": item.staged,
514
+ "modified": item.modified,
515
+ "untracked": item.untracked,
516
+ },
517
+ "locked": item.locked,
518
+ "lock_reason": item.lock_reason,
519
+ "prunable": item.prunable,
520
+ "prunable_reason": item.prunable_reason,
521
+ "metadata_missing": record is None,
522
+ "allocation_id": record.allocation_id if record else None,
523
+ "task_key": record.task_key if record else None,
524
+ "authority_key": record.authority_key if record else None,
525
+ "module": record.module if record else None,
526
+ "role": record.role if record else None,
527
+ "owner": record.owner if record else None,
528
+ "lifecycle": record.lifecycle if record else None,
529
+ "record_version": record.record_version if record else None,
530
+ "owner_action_request": action,
531
+ "wait_checkpoint": (
532
+ record.history[-1].get("checkpoint")
533
+ if record and record.lifecycle == "RETAINED" and record.history else None
534
+ ),
535
+ "created_at": record.created_at if record else None,
536
+ "last_reviewed_at": record.last_reviewed_at if record else None,
537
+ "handoff_commit": record.handoff_commit if record else None,
538
+ "finalizer_record_id": record.finalizer_record_id if record else None,
539
+ "base_commit": record.base_commit if record else None,
540
+ "task_commit": record.task_commit if record else None,
541
+ "finalizer_commit": record.finalizer_commit if record else None,
542
+ "scope_paths": list(record.scope_paths) if record else [],
543
+ "source_allocations": list(record.source_allocations) if record else [],
544
+ "integration_intent_id": intent.intent_id if intent else None,
545
+ "integration_intent_state": (
546
+ _effective_intent_state(snapshot, intent)
547
+ if intent is not None and snapshot is not None
548
+ else None
549
+ ),
550
+ "integration_intent_raw_state": intent.state if intent else None,
551
+ "source_handoff_oids": list(intent.source_handoffs) if intent else [],
552
+ "candidate_revision": intent.revision if intent else None,
553
+ "candidate_oid": intent.candidate_oid if intent else None,
554
+ "validation_evidence_id": intent.validation_evidence_id if intent else None,
555
+ "validated_oid": intent.validated_oid if intent else None,
556
+ "quarantine_expected_commit": (record.quarantine_expected_commit if record else None),
557
+ "quarantine_observed_commit": (record.quarantine_observed_commit if record else None),
558
+ "recovery_allocation_id": record.recovery_allocation_id if record else None,
559
+ "recovery_ref": record.recovery_ref if record else None,
560
+ "disposable_resources": (
561
+ [dict(resource) for resource in record.disposable_resources] if record else []
562
+ ),
563
+ "external_bindings": list(record.external_bindings) if record else [],
564
+ "release_intent_id": release_intent.intent_id if release_intent else None,
565
+ "release_intent_status": release_intent.status if release_intent else None,
566
+ "release_intent_blocker": release_intent.blocker if release_intent else None,
567
+ "release_intent_expires_at": release_intent.expires_at if release_intent else None,
568
+ "release_eligible": False,
569
+ "release_note": "not inferred from cleanliness; release guards are required",
570
+ }
571
+
572
+
573
+ def _metadata_warnings(
574
+ repository: RepositoryInventory,
575
+ policy: WorktreePolicy,
576
+ snapshot: MetadataSnapshot,
577
+ ) -> list[dict[str, str]]:
578
+ warnings = [dict(item) for item in snapshot.warnings]
579
+ live_keys = {item.key for item in repository.worktrees}
580
+ bound_allocations: defaultdict[str, list[str]] = defaultdict(list)
581
+ for key, allocation_id in snapshot.bindings.items():
582
+ bound_allocations[allocation_id].append(key)
583
+ if key not in live_keys:
584
+ warnings.append(
585
+ _warning(
586
+ "ORPHAN_BINDING",
587
+ "metadata binding has no registered worktree",
588
+ worktree_key=key,
589
+ allocation_id=allocation_id,
590
+ )
591
+ )
592
+ if allocation_id not in snapshot.records:
593
+ warnings.append(
594
+ _warning(
595
+ "MISSING_RECORD",
596
+ "binding references a missing metadata record",
597
+ worktree_key=key,
598
+ allocation_id=allocation_id,
599
+ )
600
+ )
601
+ for allocation_id in snapshot.records:
602
+ keys = bound_allocations.get(allocation_id, [])
603
+ if not keys and snapshot.records[allocation_id].lifecycle != "RELEASED":
604
+ warnings.append(
605
+ _warning(
606
+ "ORPHAN_RECORD",
607
+ "metadata record is not bound to a registered worktree",
608
+ allocation_id=allocation_id,
609
+ )
610
+ )
611
+ elif len(keys) > 1:
612
+ warnings.append(
613
+ _warning(
614
+ "DUPLICATE_BINDING",
615
+ "one allocation is bound to multiple worktrees",
616
+ allocation_id=allocation_id,
617
+ )
618
+ )
619
+
620
+ active_by_authority: defaultdict[str, list[MetadataRecord]] = defaultdict(list)
621
+ active_by_task: defaultdict[str, list[MetadataRecord]] = defaultdict(list)
622
+ for item in repository.worktrees:
623
+ record = _record_for(item, snapshot)
624
+ if record is None:
625
+ warnings.append(
626
+ _warning(
627
+ "UNMANAGED_WORKTREE",
628
+ "registered worktree has no controller metadata",
629
+ path=str(item.path),
630
+ )
631
+ )
632
+ continue
633
+ if record.owner == "unassigned":
634
+ warnings.append(
635
+ _warning(
636
+ "UNOWNED_WORKTREE",
637
+ "worktree owner is explicitly unassigned",
638
+ path=str(item.path),
639
+ allocation_id=record.allocation_id,
640
+ )
641
+ )
642
+ if record.lifecycle in ACTIVE_AUTHORITY_STATES:
643
+ active_by_task[record.task_key].append(record)
644
+ if record.role in FEATURE_AUTHORITY_ROLES:
645
+ active_by_authority[record.authority_key].append(record)
646
+ try:
647
+ reviewed = datetime.fromisoformat(record.last_reviewed_at)
648
+ age = datetime.now(TIMEZONE) - reviewed.astimezone(TIMEZONE)
649
+ if age.days >= policy.stale_warning_days:
650
+ warnings.append(
651
+ _warning(
652
+ "STALE_REVIEW",
653
+ "worktree metadata review is stale; lifecycle is not auto-expired",
654
+ path=str(item.path),
655
+ allocation_id=record.allocation_id,
656
+ )
657
+ )
658
+ except ValueError:
659
+ warnings.append(
660
+ _warning(
661
+ "INVALID_REVIEW_TIME",
662
+ "last_reviewed_at cannot be interpreted",
663
+ allocation_id=record.allocation_id,
664
+ )
665
+ )
666
+ for task_key, records in active_by_task.items():
667
+ if len(records) > 1:
668
+ warnings.append(
669
+ _warning(
670
+ "DUPLICATE_TASK",
671
+ "multiple active allocations use the same task key",
672
+ task_key=task_key,
673
+ )
674
+ )
675
+ for authority_key, records in active_by_authority.items():
676
+ if len(records) > 1:
677
+ warnings.append(
678
+ _warning(
679
+ "DUPLICATE_AUTHORITY",
680
+ "multiple active feature allocations use the same authority",
681
+ authority_key=authority_key,
682
+ )
683
+ )
684
+ if policy.policy_schema_version >= 2:
685
+ managed = [
686
+ record
687
+ for item in repository.worktrees
688
+ if not item.canonical and (record := _record_for(item, snapshot)) is not None
689
+ ]
690
+ capacity_checks = (
691
+ (
692
+ "INTEGRATION_CAPACITY_VIOLATION",
693
+ sum(record.role == "integration" and record.lifecycle not in {"PUBLISHED", "RELEASED"}
694
+ for record in managed),
695
+ policy.integration_candidate_limit,
696
+ ),
697
+ (
698
+ "FEATURE_CAPACITY_VIOLATION",
699
+ sum(
700
+ record.role in {"feature", "audit"} and record.lifecycle != "RETAINED"
701
+ for record in managed
702
+ ),
703
+ policy.feature_audit_slots,
704
+ ),
705
+ (
706
+ # Retained work is bounded by the ordinary pool it shares with writers,
707
+ # not by a separate physical sub-quota.
708
+ "ORDINARY_POOL_VIOLATION",
709
+ sum(record.role in ORDINARY_ROLES for record in managed),
710
+ policy.feature_audit_slots + policy.retained_slots,
711
+ ),
712
+ (
713
+ "EMERGENCY_CAPACITY_VIOLATION",
714
+ sum(record.role in EMERGENCY_ROLES for record in managed),
715
+ policy.emergency_slots,
716
+ ),
717
+ (
718
+ "RECOVERY_CAPACITY_VIOLATION",
719
+ sum(record.role in RECOVERY_ROLES for record in managed),
720
+ policy.recovery_reserve,
721
+ ),
722
+ )
723
+ for code, observed, limit in capacity_checks:
724
+ if policy.resource_admission and code != "INTEGRATION_CAPACITY_VIOLATION":
725
+ continue
726
+ if observed > limit:
727
+ warnings.append(
728
+ _warning(
729
+ code,
730
+ "role-specific worktree capacity is exceeded",
731
+ observed=str(observed),
732
+ limit=str(limit),
733
+ )
734
+ )
735
+ if not policy.present:
736
+ warnings.append(_warning("MISSING_POLICY", "repository worktree policy is missing"))
737
+ return warnings
738
+
739
+
740
+ def status(repository_path: Path, *, namespace_pid: int | None = None) -> dict[str, Any]:
741
+ result = envelope("status")
742
+ repository = inventory(repository_path)
743
+ diagnostics = diagnose_mounts(repository, namespace_pid=namespace_pid)
744
+ diagnosis = diagnostics["environment"]
745
+ result["mount_diagnostics"] = diagnostics
746
+ result["environment_diagnosis"] = diagnosis
747
+ policy = load_policy(repository.canonical_path)
748
+ store = MetadataStore(repository.common_dir)
749
+ try:
750
+ snapshot = store.load()
751
+ except ControllerError as exc:
752
+ raise _environment_first(exc, diagnosis) from exc
753
+ result["repository"] = _repository_payload(repository, policy, snapshot)
754
+ result["worktrees"] = _observed_worktrees(
755
+ repository, policy, snapshot, namespace_healthy=diagnosis["code"] == "HEALTHY"
756
+ )
757
+ result["orphan_observations"] = observe_repository_orphans(repository, snapshot)
758
+ result["warnings"] = (
759
+ _environment_warnings(diagnostics)
760
+ + _metadata_warnings(repository, policy, snapshot)
761
+ + (_lock_warnings(store) if diagnosis["code"] == "HEALTHY" else [])
762
+ )
763
+ return result
764
+
765
+
766
+ def _resolve_target(
767
+ repository: RepositoryInventory, target: Path | None
768
+ ) -> WorktreeGitState | None:
769
+ if target is None:
770
+ return None
771
+ try:
772
+ resolved = target.expanduser().resolve(strict=True)
773
+ except OSError as exc:
774
+ raise ControllerError("INVALID_TARGET", f"target path is unavailable: {target}") from exc
775
+ for item in repository.worktrees:
776
+ if item.path.resolve(strict=True) == resolved:
777
+ return item
778
+ raise ControllerError("UNREGISTERED_WORKTREE", f"target is not registered: {resolved}")
779
+
780
+
781
+ def plan(
782
+ repository_path: Path,
783
+ *,
784
+ task_key: str,
785
+ authority_key: str | None,
786
+ module: str,
787
+ role: str,
788
+ target: Path | None = None,
789
+ ) -> dict[str, Any]:
790
+ repository = inventory(repository_path)
791
+ policy = load_policy(repository.canonical_path)
792
+ snapshot = MetadataStore(repository.common_dir).load()
793
+ return _plan_from_snapshot(repository, policy, snapshot, task_key=task_key,
794
+ authority_key=authority_key, module=module, role=role, target=target)
795
+
796
+
797
+ def _plan_from_snapshot(
798
+ repository: RepositoryInventory,
799
+ policy: WorktreePolicy,
800
+ snapshot: MetadataSnapshot,
801
+ *,
802
+ task_key: str,
803
+ authority_key: str | None,
804
+ module: str,
805
+ role: str,
806
+ target: Path | None = None,
807
+ ) -> dict[str, Any]:
808
+ """Evaluate read-only planning; acquire still rechecks under its own lock."""
809
+ result = envelope("plan")
810
+ warnings = _metadata_warnings(repository, policy, snapshot)
811
+ result["repository"] = _repository_payload(repository, policy, snapshot)
812
+ result["worktrees"] = [
813
+ _worktree_payload(item, _record_for(item, snapshot), snapshot)
814
+ for item in repository.worktrees
815
+ ]
816
+ result["warnings"] = warnings
817
+ requested = {
818
+ "task_key": task_key,
819
+ "authority_key": authority_key,
820
+ "module": module,
821
+ "role": role,
822
+ "target": str(target.expanduser()) if target else None,
823
+ }
824
+
825
+ def decide(code: str, reason: str, **extra: Any) -> dict[str, Any]:
826
+ result["decision"] = {
827
+ "code": code,
828
+ "reason": reason,
829
+ "request": requested,
830
+ "capacity": accounting,
831
+ **extra,
832
+ }
833
+ return result
834
+
835
+ accounting = capacity_accounting(repository, policy, snapshot)
836
+ if not task_key.strip() or not module.strip() or role not in WORKTREE_ROLES:
837
+ return decide("UNKNOWN", "task_key, module, and a supported role are required")
838
+ if policy.require_authority_key and (authority_key is None or not authority_key.strip()):
839
+ return decide("UNKNOWN", "authority_key is required by repository policy")
840
+ if not policy.present:
841
+ return decide("BLOCK_DRIFT", "repository policy is missing")
842
+ capacity_drift = {
843
+ "INTEGRATION_CAPACITY_VIOLATION",
844
+ "FEATURE_CAPACITY_VIOLATION",
845
+ "EMERGENCY_CAPACITY_VIOLATION",
846
+ "RECOVERY_CAPACITY_VIOLATION",
847
+ "ORDINARY_POOL_VIOLATION",
848
+ }
849
+ if any(warning["code"] in capacity_drift for warning in warnings):
850
+ return decide("BLOCK_DRIFT", "role-specific capacity already exceeds policy")
851
+ target_item = _resolve_target(repository, target)
852
+ if target_item and target_item.canonical and role not in CANONICAL_ROLES:
853
+ return decide("BLOCK_CANONICAL", "canonical is protected from ordinary feature work")
854
+
855
+ active: list[tuple[WorktreeGitState, MetadataRecord]] = []
856
+ unmanaged: list[WorktreeGitState] = []
857
+ for item in repository.worktrees:
858
+ record = _record_for(item, snapshot)
859
+ if record is None:
860
+ unmanaged.append(item)
861
+ elif record.lifecycle in ACTIVE_AUTHORITY_STATES:
862
+ active.append((item, record))
863
+
864
+ task_matches = [(item, record) for item, record in active if record.task_key == task_key]
865
+ if task_matches:
866
+ item, record = task_matches[0]
867
+ return decide(
868
+ "REUSE",
869
+ "an active allocation already owns this task",
870
+ reuse={"path": str(item.path), "allocation_id": record.allocation_id},
871
+ )
872
+ if unmanaged and policy.require_adoption:
873
+ return decide(
874
+ "NEEDS_ADOPTION",
875
+ "all registered worktrees must be adopted before allocating another authority",
876
+ unmanaged_paths=[str(item.path) for item in unmanaged],
877
+ )
878
+ if authority_key and policy.unique_active_feature_authority:
879
+ authority_matches = [
880
+ (item, record)
881
+ for item, record in active
882
+ if record.authority_key == authority_key and record.role in FEATURE_AUTHORITY_ROLES
883
+ ]
884
+ if authority_matches:
885
+ item, record = authority_matches[0]
886
+ return decide(
887
+ "BLOCK_DUPLICATE_AUTHORITY",
888
+ "an active feature allocation already owns this authority",
889
+ conflict={
890
+ "path": str(item.path),
891
+ "allocation_id": record.allocation_id,
892
+ "task_key": record.task_key,
893
+ },
894
+ )
895
+ preparing = sum(
896
+ record.role == "integration" and record.lifecycle not in {"PUBLISHED", "RELEASED"}
897
+ for item in repository.worktrees
898
+ if not item.canonical and (record := _record_for(item, snapshot)) is not None
899
+ )
900
+ if role == "integration" and preparing >= policy.integration_candidate_limit:
901
+ return decide("BLOCK_INTEGRATION_SLOT", "integration preparation concurrency is occupied")
902
+ from .resource_admission import AdmissionLedger, allocation_request
903
+
904
+ canonical = next(item for item in repository.worktrees if item.canonical)
905
+ request = allocation_request(repository, snapshot.repository_id, policy,
906
+ target=target.expanduser().absolute() if target is not None else Path.home() / "worktrees" / repository.canonical_path.name / task_key,
907
+ base_oid=canonical.head, arguments={"task_key": task_key, "authority_key": authority_key})
908
+ budget = AdmissionLedger().plan([request])
909
+ available = "CREATE_EMERGENCY_ALLOWED" if role in EMERGENCY_ROLES else "CREATE_RECOVERY_ALLOWED" if role in RECOVERY_ROLES else "CREATE_ALLOWED"
910
+ return decide("RESOURCE_WAIT" if budget["blockers"] else available,
911
+ "target filesystem resource budget evaluated; checkout counts are statistical",
912
+ resource_admission=budget)
913
+
914
+
915
+ def guard_write(
916
+ repository_path: Path,
917
+ *,
918
+ target: Path,
919
+ task_key: str,
920
+ authority_key: str,
921
+ owner: str,
922
+ operation: str,
923
+ holder: WriterHolder | None = None,
924
+ observe: bool = True,
925
+ state: RepositoryInventory | None = None,
926
+ snapshot: MetadataSnapshot | None = None,
927
+ ) -> dict[str, Any]:
928
+ """Authorize one declared write class against the selected allocation.
929
+
930
+ This is a trusted-agent workflow gate, not same-UID process isolation. It is intentionally
931
+ read-only: lifecycle mutation remains owned by the dedicated Controller commands.
932
+ A caller already holding one repository inventory and metadata snapshot (under
933
+ its repository lock) passes them, so the decision uses that same snapshot.
934
+ """
935
+
936
+ result = status(repository_path) if observe else envelope("write-guard")
937
+ result["command"] = "write-guard"
938
+ repository = state if state is not None else inventory(repository_path)
939
+ store = MetadataStore(repository.common_dir)
940
+ if snapshot is None:
941
+ snapshot = store.load()
942
+ item = _resolve_target(repository, target)
943
+ requested = {
944
+ "path": str(target.expanduser()),
945
+ "task_key": task_key,
946
+ "authority_key": authority_key,
947
+ "owner": owner,
948
+ "operation": operation,
949
+ }
950
+
951
+ def decide(code: str, reason: str, **extra: Any) -> dict[str, Any]:
952
+ result["decision"] = {"code": code, "reason": reason, "request": requested, **extra}
953
+ return result
954
+
955
+ if item is None:
956
+ return decide("BLOCK_UNREGISTERED_WORKTREE", "write target is not a registered worktree")
957
+ record = _record_for(item, snapshot)
958
+ if record is None:
959
+ return decide("BLOCK_UNMANAGED_WORKTREE", "write target has no Controller allocation")
960
+ if not store.lock_file.is_file() or store.lock_file.is_symlink():
961
+ return decide("BLOCK_INVALID_METADATA", "governed write requires its repository lock")
962
+ if operation not in WRITE_OPERATIONS:
963
+ return decide("BLOCK_UNKNOWN_WRITE_OPERATION", "write operation is not supported")
964
+ if record.owner != owner:
965
+ return decide(
966
+ "BLOCK_OWNER_MISMATCH",
967
+ "declared owner does not own the selected allocation",
968
+ allocation_id=record.allocation_id,
969
+ )
970
+
971
+ # Recovery must remain available after task/head drift; its lifecycle commands perform the
972
+ # stronger CAS, cleanliness, lineage, and scope checks before any metadata mutation.
973
+ if operation == "recovery":
974
+ if record.role not in {"integration", "recovery", "preservation"}:
975
+ return decide(
976
+ "BLOCK_RECOVERY_ROLE",
977
+ "recovery entry is limited to integration and recovery allocations",
978
+ allocation_id=record.allocation_id,
979
+ )
980
+ if record.lifecycle in {"PUBLISHED", "RELEASED"}:
981
+ return decide(
982
+ "BLOCK_RECOVERY_LIFECYCLE",
983
+ f"lifecycle {record.lifecycle} cannot enter recovery",
984
+ allocation_id=record.allocation_id,
985
+ )
986
+ return decide(
987
+ "WRITE_ALLOWED",
988
+ "explicit recovery entry is allowed; lifecycle commands remain authoritative",
989
+ allocation_id=record.allocation_id,
990
+ allocation_task_key=record.task_key,
991
+ lifecycle=record.lifecycle,
992
+ )
993
+
994
+ if record.task_key != task_key:
995
+ return decide(
996
+ "BLOCK_TASK_IDENTITY_MISMATCH",
997
+ "declared task does not match the selected allocation",
998
+ allocation_id=record.allocation_id,
999
+ allocation_task_key=record.task_key,
1000
+ )
1001
+ if record.authority_key != authority_key:
1002
+ return decide(
1003
+ "BLOCK_AUTHORITY_IDENTITY_MISMATCH",
1004
+ "declared authority does not match the selected allocation",
1005
+ allocation_id=record.allocation_id,
1006
+ allocation_authority_key=record.authority_key,
1007
+ )
1008
+
1009
+ if operation == "feature-write":
1010
+ if record.role == "integration":
1011
+ return decide(
1012
+ "BLOCK_INTEGRATION_FEATURE_WRITE",
1013
+ "integration allocations cannot host feature implementation",
1014
+ allocation_id=record.allocation_id,
1015
+ lifecycle=record.lifecycle,
1016
+ next_action="allocate or reuse a feature worktree; use recovery if drift already exists",
1017
+ )
1018
+ if record.role not in FEATURE_AUTHORITY_ROLES:
1019
+ return decide(
1020
+ "BLOCK_FEATURE_WRITE_ROLE",
1021
+ f"role {record.role} cannot host ordinary feature implementation",
1022
+ allocation_id=record.allocation_id,
1023
+ )
1024
+ if record.lifecycle != "ACTIVE":
1025
+ return decide(
1026
+ "BLOCK_FEATURE_WRITE_LIFECYCLE",
1027
+ f"lifecycle {record.lifecycle} is frozen for feature writes",
1028
+ allocation_id=record.allocation_id,
1029
+ )
1030
+ try:
1031
+ check_writer_entitlement(
1032
+ snapshot.repository_id,
1033
+ record,
1034
+ item.path,
1035
+ snapshot.writer_leases.get(record.allocation_id),
1036
+ holder,
1037
+ )
1038
+ except ControllerError as exc:
1039
+ return decide(f"BLOCK_{exc.code}", str(exc), allocation_id=record.allocation_id)
1040
+ return decide(
1041
+ "WRITE_ALLOWED",
1042
+ "active feature allocation matches the declared task authority",
1043
+ allocation_id=record.allocation_id,
1044
+ lifecycle=record.lifecycle,
1045
+ )
1046
+
1047
+ if operation == "integration-write":
1048
+ if record.role != "integration":
1049
+ return decide(
1050
+ "BLOCK_INTEGRATION_WRITE_ROLE",
1051
+ "integration-only writes require an integration allocation",
1052
+ allocation_id=record.allocation_id,
1053
+ )
1054
+ if record.lifecycle == "HANDOFF_READY":
1055
+ drift = evidence_head_issues(item, record)
1056
+ if drift:
1057
+ return decide(
1058
+ "BLOCK_RECOVERY_REQUIRED",
1059
+ "HANDOFF_READY integration HEAD drift requires quarantine/recovery",
1060
+ allocation_id=record.allocation_id,
1061
+ finding=drift[0],
1062
+ next_action="quarantine and externalize the drift before further writes",
1063
+ )
1064
+ intent = _intent_for_allocation(snapshot, record.allocation_id)
1065
+ if intent is not None and intent.state == "READY_TO_PUBLISH":
1066
+ return decide(
1067
+ "BLOCK_VALIDATED_CANDIDATE_FROZEN",
1068
+ "validated integration candidates can only refresh, publish, or enter recovery",
1069
+ allocation_id=record.allocation_id,
1070
+ lifecycle=record.lifecycle,
1071
+ validation_evidence_id=intent.validation_evidence_id,
1072
+ )
1073
+ if intent is not None and intent.state == "NEEDS_REPREPARE":
1074
+ return decide(
1075
+ "BLOCK_REPREPARE_REQUIRED",
1076
+ "stale candidates must be rebuilt from frozen sources before further writes",
1077
+ allocation_id=record.allocation_id,
1078
+ lifecycle=record.lifecycle,
1079
+ next_action="run integration-refresh; old prepared evidence is no longer eligible",
1080
+ )
1081
+ if record.lifecycle != "ACTIVE":
1082
+ return decide(
1083
+ "BLOCK_INTEGRATION_WRITE_LIFECYCLE",
1084
+ f"lifecycle {record.lifecycle} is frozen for integration writes",
1085
+ allocation_id=record.allocation_id,
1086
+ next_action="use integration-refresh/recovery; ordinary writes remain fenced",
1087
+ )
1088
+ return decide(
1089
+ "WRITE_ALLOWED",
1090
+ "declared integration task matches the integration allocation",
1091
+ allocation_id=record.allocation_id,
1092
+ lifecycle=record.lifecycle,
1093
+ )
1094
+
1095
+ if record.lifecycle in {"QUARANTINED", "RECOVERY_REQUIRED", "RELEASED"}:
1096
+ return decide(
1097
+ "BLOCK_FINALIZATION_LIFECYCLE",
1098
+ f"lifecycle {record.lifecycle} requires its dedicated recovery/release command",
1099
+ allocation_id=record.allocation_id,
1100
+ )
1101
+ if record.lifecycle in {"HANDOFF_READY", "PRESERVED"}:
1102
+ return decide(
1103
+ "BLOCK_HANDOFF_WRITE_FENCE",
1104
+ f"lifecycle {record.lifecycle} is frozen after handoff",
1105
+ allocation_id=record.allocation_id,
1106
+ next_action="explicitly reopen the allocation before any new controlled write",
1107
+ )
1108
+ if record.role in FEATURE_AUTHORITY_ROLES:
1109
+ if record.lifecycle != "ACTIVE":
1110
+ return decide("BLOCK_FINALIZATION_LIFECYCLE", "writer allocation is not active")
1111
+ try:
1112
+ check_writer_entitlement(
1113
+ snapshot.repository_id,
1114
+ record,
1115
+ item.path,
1116
+ snapshot.writer_leases.get(record.allocation_id),
1117
+ holder,
1118
+ )
1119
+ except ControllerError as exc:
1120
+ return decide(f"BLOCK_{exc.code}", str(exc), allocation_id=record.allocation_id)
1121
+ return decide(
1122
+ "WRITE_ALLOWED",
1123
+ "declared finalization task matches the allocation",
1124
+ allocation_id=record.allocation_id,
1125
+ lifecycle=record.lifecycle,
1126
+ )
1127
+
1128
+
1129
+ def adopt(
1130
+ repository_path: Path,
1131
+ *,
1132
+ target: Path,
1133
+ task_key: str,
1134
+ authority_key: str,
1135
+ module: str,
1136
+ role: str,
1137
+ owner: str,
1138
+ lifecycle: str,
1139
+ base_commit: str | None = None,
1140
+ task_commit: str | None = None,
1141
+ scope_paths: tuple[str, ...] = (),
1142
+ source_allocations: tuple[str, ...] = (),
1143
+ external_bindings: tuple[str, ...] = (),
1144
+ ) -> dict[str, Any]:
1145
+ result = envelope("adopt")
1146
+ if role not in WORKTREE_ROLES:
1147
+ raise ControllerError("INVALID_ROLE", f"unsupported role: {role}")
1148
+ if lifecycle not in LIFECYCLE_STATES:
1149
+ raise ControllerError("INVALID_LIFECYCLE", f"unsupported lifecycle: {lifecycle}")
1150
+ repository_before = inventory(repository_path)
1151
+ target_item = _resolve_target(repository_before, target)
1152
+ if target_item is None:
1153
+ raise ControllerError("INVALID_TARGET", "adopt target is required")
1154
+ if target_item.canonical and role not in CANONICAL_ROLES:
1155
+ raise ControllerError("BLOCK_CANONICAL", "canonical requires a control-plane role")
1156
+ normalized_scopes = normalize_scope_paths(scope_paths)
1157
+ base_oid = resolve_commit(target_item.path, base_commit) if base_commit else None
1158
+ task_oid = resolve_commit(target_item.path, task_commit) if task_commit else None
1159
+ before = git_state_fingerprint(repository_before)
1160
+ store = MetadataStore(repository_before.common_dir)
1161
+ created = False
1162
+ with store.lock(exclusive=True, create=True):
1163
+ repository_locked = inventory(repository_path)
1164
+ locked_item = _resolve_target(repository_locked, target)
1165
+ if locked_item is None or locked_item.key != target_item.key:
1166
+ raise ControllerError("BLOCK_DRIFT", "worktree identity changed during adoption")
1167
+ snapshot = store._load_unlocked()
1168
+ allocation_id = snapshot.bindings.get(locked_item.key)
1169
+ if allocation_id is not None:
1170
+ record = snapshot.records.get(allocation_id)
1171
+ if record is None:
1172
+ raise ControllerError("BLOCK_DRIFT", "binding references a missing record")
1173
+ expected = (
1174
+ task_key.strip(),
1175
+ authority_key.strip(),
1176
+ module.strip(),
1177
+ role,
1178
+ owner.strip(),
1179
+ lifecycle,
1180
+ base_oid,
1181
+ task_oid,
1182
+ normalized_scopes,
1183
+ tuple(source_allocations),
1184
+ )
1185
+ actual = (
1186
+ record.task_key,
1187
+ record.authority_key,
1188
+ record.module,
1189
+ record.role,
1190
+ record.owner,
1191
+ record.lifecycle,
1192
+ record.base_commit,
1193
+ record.task_commit,
1194
+ record.scope_paths,
1195
+ record.source_allocations,
1196
+ )
1197
+ if actual != expected:
1198
+ raise ControllerError(
1199
+ "ADOPTION_CONFLICT",
1200
+ "worktree is already adopted with different metadata; use lifecycle commands",
1201
+ )
1202
+ else:
1203
+ store.initialize_repository()
1204
+ record = new_record(
1205
+ task_key=task_key,
1206
+ authority_key=authority_key,
1207
+ module=module,
1208
+ role=role,
1209
+ owner=owner,
1210
+ lifecycle=lifecycle,
1211
+ base_commit=base_oid,
1212
+ task_commit=task_oid,
1213
+ scope_paths=normalized_scopes,
1214
+ source_allocations=tuple(source_allocations),
1215
+ external_bindings=external_bindings,
1216
+ )
1217
+ store.write_record(
1218
+ locked_item.key,
1219
+ record,
1220
+ registration=None if locked_item.canonical else locked_item.git_dir,
1221
+ )
1222
+ created = True
1223
+ repository_after = inventory(repository_path)
1224
+ after = git_state_fingerprint(repository_after)
1225
+ if before != after:
1226
+ raise ControllerError(
1227
+ "GIT_STATE_CHANGED", "Git refs, index, worktrees, or status changed during adoption"
1228
+ )
1229
+ policy = load_policy(repository_after.canonical_path)
1230
+ snapshot_after = store.load()
1231
+ result["repository"] = _repository_payload(repository_after, policy, snapshot_after)
1232
+ result["worktrees"] = [
1233
+ _worktree_payload(item, _record_for(item, snapshot_after), snapshot_after)
1234
+ for item in repository_after.worktrees
1235
+ ]
1236
+ result["warnings"] = _metadata_warnings(repository_after, policy, snapshot_after)
1237
+ result["decision"] = {
1238
+ "code": "ADOPTED" if created else "ALREADY_ADOPTED",
1239
+ "reason": "controller metadata created" if created else "metadata already matched",
1240
+ "allocation_id": record.allocation_id,
1241
+ "path": str(target_item.path),
1242
+ }
1243
+ return result
1244
+
1245
+
1246
+ def _semantic_warnings(
1247
+ repository: RepositoryInventory, snapshot: MetadataSnapshot, policy: WorktreePolicy
1248
+ ) -> list[dict[str, str]]:
1249
+ warnings: list[dict[str, str]] = []
1250
+ items_by_allocation: dict[str, tuple[WorktreeGitState, MetadataRecord]] = {}
1251
+ for item in repository.worktrees:
1252
+ record = _record_for(item, snapshot)
1253
+ if record is None:
1254
+ continue
1255
+ items_by_allocation[record.allocation_id] = (item, record)
1256
+ if record.role == "integration":
1257
+ issues = list(integration_candidate_issues(item, record, snapshot))
1258
+ else:
1259
+ issues = list(evidence_head_issues(item, record))
1260
+ if record.lifecycle in EVIDENCE_LIFECYCLES:
1261
+ issues.extend(
1262
+ path_scope_issues(
1263
+ item.path,
1264
+ record,
1265
+ item.head,
1266
+ publication_ref=publication_ref(item.path, policy),
1267
+ )
1268
+ )
1269
+ warnings.extend(issues)
1270
+ if record.lifecycle == "RELEASE_READY":
1271
+ from .delivered_closeout import verify_receipt
1272
+
1273
+ try:
1274
+ verify_receipt(MetadataStore(repository.common_dir), item, record, snapshot.repository_id)
1275
+ except ControllerError as exc:
1276
+ warnings.append(_warning(exc.code, str(exc), allocation_id=record.allocation_id))
1277
+ if record.lifecycle == "ARCHIVED":
1278
+ from .delivered_closeout import verify_archive_receipt
1279
+
1280
+ try:
1281
+ verify_archive_receipt(
1282
+ MetadataStore(repository.common_dir), item, record, snapshot.repository_id
1283
+ )
1284
+ except ControllerError as exc:
1285
+ warnings.append(_warning(exc.code, str(exc), allocation_id=record.allocation_id))
1286
+ if (
1287
+ issues
1288
+ and record.role == "integration"
1289
+ and record.lifecycle not in {"QUARANTINED", "RECOVERY_REQUIRED"}
1290
+ ):
1291
+ warnings.append(
1292
+ _warning(
1293
+ "RECOVERY_REQUIRED",
1294
+ "integration semantic drift requires explicit quarantine and recovery",
1295
+ allocation_id=record.allocation_id,
1296
+ path=str(item.path),
1297
+ )
1298
+ )
1299
+ if record.lifecycle in {"QUARANTINED", "RECOVERY_REQUIRED"}:
1300
+ warnings.append(
1301
+ _warning(
1302
+ record.lifecycle,
1303
+ "allocation is held for explicit governance recovery",
1304
+ allocation_id=record.allocation_id,
1305
+ path=str(item.path),
1306
+ )
1307
+ )
1308
+
1309
+ lifecycle_for_intent = {
1310
+ "PREPARING_INTEGRATION": {"ACTIVE"},
1311
+ "READY_TO_PUBLISH": {"HANDOFF_READY"},
1312
+ "NEEDS_REPREPARE": {"ACTIVE"},
1313
+ "PUBLISHING": {"INTEGRATING"},
1314
+ "RECOVERY_REQUIRED": {"QUARANTINED", "RECOVERY_REQUIRED", "RELEASED"},
1315
+ "ATTENTION_REQUIRED": {"BLOCKED"},
1316
+ "REMOTE_VERIFIED": {"PUBLISHED", "RELEASED"},
1317
+ "RELEASED": {"RELEASED"},
1318
+ }
1319
+ for intent in snapshot.integration_intents.values():
1320
+ effective_state = _effective_intent_state(snapshot, intent)
1321
+ if effective_state in {"QUEUED", "WAITING_SCOPE", "ABANDONED"}:
1322
+ continue
1323
+ if unallocated_integration_conflict(intent):
1324
+ warnings.append(_warning(
1325
+ "INTEGRATION_CONFLICT_PENDING",
1326
+ "pre-allocation conflict needs owner retry and explicit resolution",
1327
+ severity="informational", intent_id=intent.intent_id,
1328
+ ))
1329
+ continue
1330
+ record = (
1331
+ snapshot.records.get(intent.allocation_id) if intent.allocation_id is not None else None
1332
+ )
1333
+ if record is None:
1334
+ warnings.append(
1335
+ _warning(
1336
+ "INTEGRATION_INTENT_DRIFT",
1337
+ "claimed integration intent has no allocation record",
1338
+ intent_id=intent.intent_id,
1339
+ )
1340
+ )
1341
+ continue
1342
+ if effective_state == "RELEASED" and intent.state != "RELEASED":
1343
+ warnings.append(
1344
+ _warning(
1345
+ "INTEGRATION_INTENT_RECONCILED",
1346
+ "immutable reconciliation makes historical intent drift non-blocking",
1347
+ severity="informational",
1348
+ intent_id=intent.intent_id,
1349
+ allocation_id=record.allocation_id,
1350
+ )
1351
+ )
1352
+ continue
1353
+ expected_lifecycles = lifecycle_for_intent.get(effective_state, set())
1354
+ if (
1355
+ record.role != "integration"
1356
+ or record.task_key != intent.task_key
1357
+ or record.authority_key != intent.authority_key
1358
+ or record.source_allocations != intent.source_allocations
1359
+ or record.scope_paths != intent.scope_paths
1360
+ or record.lifecycle not in expected_lifecycles
1361
+ ):
1362
+ warnings.append(
1363
+ _warning(
1364
+ "INTEGRATION_INTENT_DRIFT",
1365
+ "integration intent and candidate allocation identity differ",
1366
+ intent_id=intent.intent_id,
1367
+ allocation_id=record.allocation_id,
1368
+ )
1369
+ )
1370
+ if intent.candidate_oid != record.task_commit:
1371
+ warnings.append(
1372
+ _warning(
1373
+ "INTEGRATION_INTENT_DRIFT",
1374
+ "integration intent candidate OID differs from allocation evidence",
1375
+ intent_id=intent.intent_id,
1376
+ allocation_id=record.allocation_id,
1377
+ )
1378
+ )
1379
+ if intent.schema_version >= 3 and effective_state in {
1380
+ "READY_TO_PUBLISH",
1381
+ "PUBLISHING",
1382
+ "REMOTE_VERIFIED",
1383
+ "RELEASED",
1384
+ }:
1385
+ prepared = (
1386
+ snapshot.prepared_candidate_receipts.get(intent.prepared_receipt_id)
1387
+ if intent.prepared_receipt_id is not None
1388
+ else None
1389
+ )
1390
+ matched = items_by_allocation.get(record.allocation_id)
1391
+ live_tree_drift = (
1392
+ matched is not None
1393
+ and matched[0].head == intent.prepared_commit_oid
1394
+ and commit_tree_oid(matched[0].path, matched[0].head) != intent.prepared_tree_oid
1395
+ )
1396
+ if (
1397
+ prepared is None
1398
+ or prepared.repository_id != snapshot.repository_id
1399
+ or prepared.intent_id != intent.intent_id
1400
+ or prepared.allocation_id != record.allocation_id
1401
+ or prepared.attempt != intent.attempt
1402
+ or prepared.base_origin_main_oid != intent.prepared_base_origin_main_oid
1403
+ or prepared.candidate_oid != intent.prepared_commit_oid
1404
+ or prepared.tree_oid != intent.prepared_tree_oid
1405
+ or prepared.scope_digest != intent.prepared_scope_digest
1406
+ or prepared.prepare_identity != intent.prepare_identity
1407
+ or prepared.validation_evidence_id != intent.validation_evidence_id
1408
+ or prepared.tests_receipt_id != intent.tests_receipt_id
1409
+ or prepared.snapshot_receipt_id != intent.snapshot_receipt_id
1410
+ or live_tree_drift
1411
+ ):
1412
+ warnings.append(
1413
+ _warning(
1414
+ "PREPARED_RECEIPT_DRIFT",
1415
+ "prepared candidate receipt no longer matches intent and tree evidence",
1416
+ severity="blocking",
1417
+ intent_id=intent.intent_id,
1418
+ allocation_id=record.allocation_id,
1419
+ )
1420
+ )
1421
+
1422
+ # Historical publications join release evidence by exact identity, once per snapshot.
1423
+ released_publications = {
1424
+ (release_receipt.allocation_id, release_receipt.released_record_version,
1425
+ plan.publication_receipt_id)
1426
+ for release_receipt in snapshot.release_receipts.values()
1427
+ if (plan := snapshot.release_plans.get(release_receipt.plan_id)) is not None
1428
+ }
1429
+ receipts_by_allocation: defaultdict[str, int] = defaultdict(int)
1430
+ for receipt in snapshot.publication_receipts.values():
1431
+ receipts_by_allocation[receipt.allocation_id] += 1
1432
+ record = snapshot.records.get(receipt.allocation_id)
1433
+ expected_lifecycle = "RELEASED" if receipt.status == "RELEASED" else "PUBLISHED"
1434
+ identity_drift = (
1435
+ receipt.repository_id != snapshot.repository_id
1436
+ or record is None
1437
+ or record.task_commit != receipt.candidate_oid
1438
+ or record.finalizer_record_id != receipt.finalizer_record_id
1439
+ )
1440
+ if not identity_drift and record is not None and record.lifecycle != expected_lifecycle:
1441
+ immutable_release_evidence = (
1442
+ record.allocation_id, record.record_version, receipt.receipt_id
1443
+ ) in released_publications
1444
+ if immutable_release_evidence and record.lifecycle == "RELEASED":
1445
+ continue
1446
+ reconciliations = [
1447
+ reconciliation
1448
+ for reconciliation in snapshot.reconciliation_receipts.values()
1449
+ if reconciliation.source_receipt_id == receipt.receipt_id
1450
+ and reconciliation.allocation_id == record.allocation_id
1451
+ and reconciliation.observed_record_version == record.record_version
1452
+ and reconciliation.result == "RELEASE_STATE_ACKNOWLEDGED"
1453
+ ]
1454
+ if len(reconciliations) == 1 and record.lifecycle == "RELEASED":
1455
+ warnings.append(
1456
+ _warning(
1457
+ "PUBLICATION_RECEIPT_RECONCILED",
1458
+ "immutable publication receipt drift has reconciliation evidence",
1459
+ severity="informational",
1460
+ receipt_id=receipt.receipt_id,
1461
+ allocation_id=receipt.allocation_id,
1462
+ )
1463
+ )
1464
+ continue
1465
+ if (
1466
+ not reconciliations
1467
+ and receipt.status == "REMOTE_VERIFIED"
1468
+ and receipt.lease_released_at is not None
1469
+ and record.lifecycle == "RELEASED"
1470
+ ):
1471
+ warnings.append(
1472
+ _warning(
1473
+ "PUBLICATION_RECEIPT_DRIFT",
1474
+ "released historical receipt needs idempotent reconciliation",
1475
+ severity="degraded",
1476
+ receipt_id=receipt.receipt_id,
1477
+ allocation_id=receipt.allocation_id,
1478
+ )
1479
+ )
1480
+ continue
1481
+ if identity_drift or (record is not None and record.lifecycle != expected_lifecycle):
1482
+ warnings.append(
1483
+ _warning(
1484
+ "PUBLICATION_RECEIPT_DRIFT",
1485
+ "publication receipt no longer matches repository and candidate evidence",
1486
+ severity="blocking",
1487
+ receipt_id=receipt.receipt_id,
1488
+ allocation_id=receipt.allocation_id,
1489
+ )
1490
+ )
1491
+ for allocation_id, count in receipts_by_allocation.items():
1492
+ if count > 1:
1493
+ warnings.append(
1494
+ _warning(
1495
+ "PUBLICATION_RECEIPT_DRIFT",
1496
+ "one candidate allocation has multiple publication receipts",
1497
+ severity="blocking",
1498
+ allocation_id=allocation_id,
1499
+ count=str(count),
1500
+ )
1501
+ )
1502
+
1503
+ lease = snapshot.publication_lease
1504
+ if lease is not None:
1505
+ if lease.repository_id not in {
1506
+ snapshot.repository_id,
1507
+ "00000000-0000-0000-0000-000000000000",
1508
+ }:
1509
+ warnings.append(
1510
+ _warning(
1511
+ "PUBLICATION_LEASE_DRIFT",
1512
+ "publication lease repository identity differs",
1513
+ allocation_id=lease.allocation_id,
1514
+ )
1515
+ )
1516
+ try:
1517
+ if datetime.fromisoformat(lease.expires_at) <= datetime.now(TIMEZONE):
1518
+ warnings.append(
1519
+ _warning(
1520
+ "PUBLICATION_LEASE_EXPIRED",
1521
+ "publication lease expired and requires remote-fact recovery",
1522
+ allocation_id=lease.allocation_id,
1523
+ )
1524
+ )
1525
+ except ValueError:
1526
+ warnings.append(
1527
+ _warning(
1528
+ "PUBLICATION_LEASE_DRIFT",
1529
+ "publication lease expiry is invalid",
1530
+ allocation_id=lease.allocation_id,
1531
+ )
1532
+ )
1533
+ matched = items_by_allocation.get(lease.allocation_id)
1534
+ if matched is None:
1535
+ warnings.append(
1536
+ _warning(
1537
+ "PUBLICATION_LEASE_DRIFT",
1538
+ "publication lease references no registered allocation",
1539
+ allocation_id=lease.allocation_id,
1540
+ )
1541
+ )
1542
+ else:
1543
+ item, record = matched
1544
+ if (
1545
+ record.lifecycle != "INTEGRATING"
1546
+ or item.head != lease.candidate_oid
1547
+ or record.finalizer_record_id != lease.validation_evidence_id
1548
+ ):
1549
+ warnings.append(
1550
+ _warning(
1551
+ "PUBLICATION_LEASE_DRIFT",
1552
+ "publication lease no longer matches candidate evidence",
1553
+ allocation_id=lease.allocation_id,
1554
+ )
1555
+ )
1556
+ else:
1557
+ warnings.append(
1558
+ _warning(
1559
+ "ACTIVE_PUBLICATION_LEASE",
1560
+ "main publication authority is currently leased",
1561
+ allocation_id=lease.allocation_id,
1562
+ )
1563
+ )
1564
+ try:
1565
+ tracking_head = resolve_commit(
1566
+ repository.canonical_path,
1567
+ f"refs/remotes/{lease.remote}/{lease.branch}",
1568
+ )
1569
+ except ControllerError:
1570
+ tracking_head = None
1571
+ if tracking_head != lease.expected_main_oid:
1572
+ warnings.append(
1573
+ _warning(
1574
+ "PUBLICATION_REMOTE_DRIFT",
1575
+ "local publication tracking ref changed after lease acquisition",
1576
+ allocation_id=lease.allocation_id,
1577
+ )
1578
+ )
1579
+ for allocation_id, (_item, record) in items_by_allocation.items():
1580
+ if record.lifecycle == "INTEGRATING" and (
1581
+ lease is None or lease.allocation_id != allocation_id
1582
+ ):
1583
+ warnings.append(
1584
+ _warning(
1585
+ "PUBLICATION_LEASE_DRIFT",
1586
+ "integrating lifecycle has no matching publication lease",
1587
+ allocation_id=allocation_id,
1588
+ )
1589
+ )
1590
+ return warnings
1591
+
1592
+
1593
+ def doctor(repository_path: Path, *, namespace_pid: int | None = None) -> dict[str, Any]:
1594
+ result = envelope("doctor")
1595
+ try:
1596
+ repository = inventory(repository_path)
1597
+ except ControllerError as exc:
1598
+ result["environment_diagnosis"] = {
1599
+ "code": "UNKNOWN",
1600
+ "facts": ["GIT_INVENTORY_UNAVAILABLE"],
1601
+ }
1602
+ result["errors"].append({"code": exc.code, "message": str(exc)})
1603
+ result["decision"] = {
1604
+ "code": "FAIL",
1605
+ "reason": "Git inventory is unavailable; mount and lock authority cannot be confirmed",
1606
+ "primary_diagnosis": "UNKNOWN",
1607
+ }
1608
+ return result
1609
+ diagnostics = diagnose_mounts(repository, namespace_pid=namespace_pid)
1610
+ diagnosis = diagnostics["environment"]
1611
+ result["mount_diagnostics"] = diagnostics
1612
+ result["environment_diagnosis"] = diagnosis
1613
+ result["warnings"] = _environment_warnings(diagnostics)
1614
+ try:
1615
+ policy = load_policy(repository.canonical_path)
1616
+ except ControllerError as exc:
1617
+ result["errors"].append({"code": exc.code, "message": str(exc)})
1618
+ from .policy import default_policy
1619
+
1620
+ policy = default_policy(repository.canonical_path)
1621
+ store = MetadataStore(repository.common_dir)
1622
+ try:
1623
+ snapshot = store.load()
1624
+ except ControllerError as exc:
1625
+ error = _environment_first(exc, diagnosis)
1626
+ result["repository"] = {
1627
+ "canonical_path": str(repository.canonical_path),
1628
+ "common_dir": str(repository.common_dir),
1629
+ "worktree_count": len(repository.worktrees),
1630
+ "normal_limit": policy.normal_limit,
1631
+ "hard_limit": policy.hard_limit,
1632
+ "metadata_path": str(store.root),
1633
+ }
1634
+ result["errors"].append({"code": error.code, "message": str(error)})
1635
+ result["decision"] = {
1636
+ "code": "FAIL",
1637
+ "reason": "repository environment or controller metadata is unavailable",
1638
+ "primary_diagnosis": _primary_diagnosis(
1639
+ diagnosis, result["warnings"] + result["errors"], failed=True
1640
+ ),
1641
+ }
1642
+ return result
1643
+ warnings = _metadata_warnings(repository, policy, snapshot)
1644
+ result["repository"] = _repository_payload(repository, policy, snapshot)
1645
+ result["worktrees"] = _observed_worktrees(
1646
+ repository, policy, snapshot, namespace_healthy=diagnosis["code"] == "HEALTHY"
1647
+ )
1648
+ result["orphan_observations"] = observe_repository_orphans(repository, snapshot)
1649
+ result["warnings"] += (
1650
+ warnings
1651
+ + _semantic_warnings(repository, snapshot, policy)
1652
+ + (_lock_warnings(store) if diagnosis["code"] == "HEALTHY" else [])
1653
+ )
1654
+ transaction_count = 0
1655
+ temp_count = 0
1656
+ retirement_grants: list[dict[str, str]] = []
1657
+ retirement_unattested: list[dict[str, str]] = []
1658
+ historical_transactions = []
1659
+ transaction_fields = {
1660
+ "schema_version",
1661
+ "transaction_id",
1662
+ "repository_id",
1663
+ "command",
1664
+ "phase",
1665
+ "target_path",
1666
+ "allocation_id",
1667
+ "branch",
1668
+ "expected_head",
1669
+ "expected_record_version",
1670
+ "created_at",
1671
+ "updated_at",
1672
+ }
1673
+ try:
1674
+ with store.lock(exclusive=False):
1675
+ if store.transactions_dir.exists():
1676
+ for transaction_path in sorted(store.transactions_dir.iterdir()):
1677
+ from .transaction_scope import is_atomic_temp, terminal_state
1678
+
1679
+ if is_atomic_temp(transaction_path.name):
1680
+ temp_count += 1
1681
+ continue
1682
+ if transaction_path.suffix != ".json" or not transaction_path.is_file():
1683
+ raise ControllerError("INVALID_TRANSACTION", "unknown transaction entry")
1684
+ state = terminal_state(store, transaction_path.stem)
1685
+ if state is not None:
1686
+ historical_transactions.append(
1687
+ {"transaction_id": transaction_path.stem, "state": state}
1688
+ )
1689
+ continue
1690
+ transaction = store._read_json(transaction_path)
1691
+ fields = set(transaction)
1692
+ release_fields = transaction_fields | {"release_plan_id"}
1693
+ teardown_fields = release_fields | {"teardown_receipt_id"}
1694
+ refresh_fields = transaction_fields | {"new_head", "intent_id"}
1695
+ cleanup_fields = transaction_fields | {"cleanup_plan"}
1696
+ cleanup_transaction = fields in (
1697
+ cleanup_fields,
1698
+ cleanup_fields | {"actions"},
1699
+ cleanup_fields | {"actions", "failure"},
1700
+ )
1701
+ teardown_transaction = fields == teardown_fields
1702
+ planned_release_transaction = fields in (release_fields, teardown_fields)
1703
+ refresh_transaction = fields == refresh_fields
1704
+ teardown_phases = {
1705
+ "TEARDOWN_PREPARED",
1706
+ "VENV_REMOVAL_STARTED",
1707
+ "VENV_REMOVED",
1708
+ }
1709
+ phase = transaction.get("phase")
1710
+ if (
1711
+ (
1712
+ fields != transaction_fields
1713
+ and fields != release_fields
1714
+ and fields != teardown_fields
1715
+ and fields != refresh_fields
1716
+ and not cleanup_transaction
1717
+ )
1718
+ or transaction.get("schema_version") not in {1, 2, 3, 4}
1719
+ or transaction.get("command")
1720
+ not in {"acquire", "release", "integration-refresh", "resource-clean"}
1721
+ or phase
1722
+ not in {
1723
+ "PREPARED",
1724
+ "TEARDOWN_PREPARED",
1725
+ "VENV_REMOVAL_STARTED",
1726
+ "VENV_REMOVED",
1727
+ "WORKTREE_ADDED",
1728
+ "METADATA_WRITTEN",
1729
+ "WORKTREE_REMOVED",
1730
+ "RELEASED_RECORDED",
1731
+ "REFRESH_PREPARED",
1732
+ "DETACHED_AT_NEW_HEAD",
1733
+ "BRANCH_UPDATED",
1734
+ }
1735
+ or (teardown_transaction and transaction.get("command") != "release")
1736
+ or (planned_release_transaction and transaction.get("command") != "release")
1737
+ or (
1738
+ transaction.get("schema_version") == 3
1739
+ and transaction.get("command") == "release"
1740
+ and not planned_release_transaction
1741
+ )
1742
+ or (
1743
+ refresh_transaction
1744
+ and transaction.get("command") != "integration-refresh"
1745
+ )
1746
+ or (phase in teardown_phases and not teardown_transaction)
1747
+ or (
1748
+ transaction.get("command") == "resource-clean"
1749
+ and (not cleanup_transaction or phase != "PREPARED")
1750
+ )
1751
+ or (cleanup_transaction and transaction.get("command") != "resource-clean")
1752
+ or (
1753
+ phase in {"REFRESH_PREPARED", "DETACHED_AT_NEW_HEAD", "BRANCH_UPDATED"}
1754
+ and not refresh_transaction
1755
+ )
1756
+ or (
1757
+ teardown_transaction
1758
+ and transaction.get("teardown_receipt_id")
1759
+ != transaction.get("transaction_id")
1760
+ )
1761
+ ):
1762
+ raise ControllerError(
1763
+ "INVALID_TRANSACTION",
1764
+ f"transaction journal schema is invalid: {transaction_path}",
1765
+ )
1766
+ if cleanup_transaction:
1767
+ cleanup_plan = transaction.get("cleanup_plan")
1768
+ if not isinstance(cleanup_plan, dict) or any(
1769
+ cleanup_plan.get(key) != transaction[key]
1770
+ for key in (
1771
+ "repository_id",
1772
+ "allocation_id",
1773
+ "target_path",
1774
+ "expected_head",
1775
+ "expected_record_version",
1776
+ )
1777
+ ):
1778
+ raise ControllerError(
1779
+ "INVALID_TRANSACTION", "cleanup plan does not match journal"
1780
+ )
1781
+ if "actions" in transaction:
1782
+ actions = transaction["actions"]
1783
+ if (
1784
+ not isinstance(actions, list)
1785
+ or not all(
1786
+ isinstance(action, dict)
1787
+ and set(action) == {"path", "status"}
1788
+ and action["status"]
1789
+ in {"PENDING", "STARTED", "COMPLETED", "FAILED_OR_PARTIAL"}
1790
+ for action in actions
1791
+ )
1792
+ or [action["path"] for action in actions]
1793
+ != cleanup_plan.get("cleanup_paths")
1794
+ ):
1795
+ raise ControllerError(
1796
+ "INVALID_TRANSACTION", "cleanup action progress is invalid"
1797
+ )
1798
+ if "failure" in transaction:
1799
+ failure = transaction["failure"]
1800
+ if (
1801
+ not isinstance(failure, dict)
1802
+ or set(failure) != {"code", "errno"}
1803
+ or not isinstance(failure["code"], str)
1804
+ or (
1805
+ failure["errno"] is not None
1806
+ and not isinstance(failure["errno"], int)
1807
+ )
1808
+ ):
1809
+ raise ControllerError(
1810
+ "INVALID_TRANSACTION", "cleanup failure evidence is invalid"
1811
+ )
1812
+ if planned_release_transaction:
1813
+ plan_id = transaction.get("release_plan_id")
1814
+ plan = (
1815
+ snapshot.release_plans.get(plan_id)
1816
+ if isinstance(plan_id, str)
1817
+ else None
1818
+ )
1819
+ if (
1820
+ plan is None
1821
+ or plan.repository_id != transaction["repository_id"]
1822
+ or plan.allocation_id != transaction["allocation_id"]
1823
+ or plan.target_path != transaction["target_path"]
1824
+ or plan.branch != transaction["branch"]
1825
+ or plan.expected_head != transaction["expected_head"]
1826
+ or plan.expected_record_version
1827
+ != transaction["expected_record_version"]
1828
+ ):
1829
+ raise ControllerError(
1830
+ "INVALID_TRANSACTION",
1831
+ f"release plan does not match journal: {transaction_path}",
1832
+ )
1833
+ if teardown_transaction:
1834
+ receipt = store.load_teardown_receipt(
1835
+ str(transaction["teardown_receipt_id"])
1836
+ )
1837
+ expected_results = {
1838
+ "TEARDOWN_PREPARED": {"prepared"},
1839
+ "VENV_REMOVAL_STARTED": {"removal_started"},
1840
+ "VENV_REMOVED": {"removed"},
1841
+ "PREPARED": {"removed"},
1842
+ "WORKTREE_REMOVED": {"removed"},
1843
+ "RELEASED_RECORDED": {"removed", "released"},
1844
+ }.get(phase)
1845
+ if (
1846
+ expected_results is None
1847
+ or receipt["result"] not in expected_results
1848
+ or receipt["repository_id"] != transaction["repository_id"]
1849
+ or receipt["allocation_id"] != transaction["allocation_id"]
1850
+ or receipt["worktree_path"] != transaction["target_path"]
1851
+ or receipt["worktree_head"] != transaction["expected_head"]
1852
+ ):
1853
+ raise ControllerError(
1854
+ "INVALID_TRANSACTION",
1855
+ f"teardown receipt does not match journal: {transaction_path}",
1856
+ )
1857
+ transaction_count += 1
1858
+ outstanding, unattested = _retirement_execution_findings(store)
1859
+ retirement_grants.extend(outstanding)
1860
+ retirement_unattested.extend(unattested)
1861
+ except ControllerError as exc:
1862
+ error = _environment_first(exc, diagnosis)
1863
+ result["errors"].append({"code": error.code, "message": str(error)})
1864
+ if transaction_count:
1865
+ result["warnings"].append(
1866
+ _warning(
1867
+ "INTERRUPTED_TRANSACTION",
1868
+ "controller transaction journal requires review",
1869
+ count=str(transaction_count),
1870
+ )
1871
+ )
1872
+ if temp_count:
1873
+ result["warnings"].append(
1874
+ _warning(
1875
+ "ATOMIC_TEMP_RESIDUE",
1876
+ "interrupted atomic write temp files remain in transactions/ and are ignored",
1877
+ count=str(temp_count),
1878
+ )
1879
+ )
1880
+ for grant in retirement_grants:
1881
+ result["warnings"].append(
1882
+ _warning(
1883
+ "RETIREMENT_AUTHORIZATION_OUTSTANDING",
1884
+ "a retirement execution authorization is open for this repository",
1885
+ authorization_id=grant["authorization_id"],
1886
+ plan_id=grant["plan_id"],
1887
+ operation=grant["operation"],
1888
+ branch=grant["branch"],
1889
+ expires_at=grant["expires_at"],
1890
+ severity="degraded",
1891
+ )
1892
+ )
1893
+ for removal in retirement_unattested:
1894
+ result["warnings"].append(
1895
+ _warning(
1896
+ removal["code"],
1897
+ "a retirement ref is absent with no Controller-observed authorized transition",
1898
+ authorization_id=removal["authorization_id"],
1899
+ plan_id=removal["plan_id"],
1900
+ operation=removal["operation"],
1901
+ ref=removal.get("ref", ""),
1902
+ severity="blocking",
1903
+ )
1904
+ )
1905
+ failure_codes = {
1906
+ "ORPHAN_BINDING",
1907
+ "ORPHAN_RECORD",
1908
+ "MISSING_RECORD",
1909
+ "DUPLICATE_BINDING",
1910
+ "DUPLICATE_AUTHORITY",
1911
+ "DUPLICATE_TASK",
1912
+ "BUDGET_VIOLATION",
1913
+ "INTEGRATION_CAPACITY_VIOLATION",
1914
+ "FEATURE_CAPACITY_VIOLATION",
1915
+ "EMERGENCY_CAPACITY_VIOLATION",
1916
+ "RECOVERY_CAPACITY_VIOLATION",
1917
+ "MISSING_POLICY",
1918
+ "MISSING_REPO_METADATA",
1919
+ "REPOSITORY_LOCK_MISSING",
1920
+ "REPOSITORY_LOCK_INVALID",
1921
+ "INTERRUPTED_TRANSACTION",
1922
+ "MISSING_TASK_COMMIT",
1923
+ "TASK_HEAD_MISMATCH",
1924
+ "DELIVERED_CLOSEOUT_INVALID",
1925
+ "RETAINED_ARCHIVE_INVALID",
1926
+ "HANDOFF_HEAD_MISMATCH",
1927
+ "MISSING_FINALIZER_COMMIT",
1928
+ "FINALIZER_HEAD_MISMATCH",
1929
+ "MISSING_INTEGRATION_BASE",
1930
+ "INTEGRATION_BASE_DIVERGED",
1931
+ "MISSING_INTEGRATION_SOURCE",
1932
+ "MISSING_SOURCE_ALLOCATION",
1933
+ "SOURCE_EVIDENCE_MISMATCH",
1934
+ "MISSING_SOURCE_SCOPE",
1935
+ "INTEGRATION_SCOPE_MISMATCH",
1936
+ "COMMIT_SCOPE_VIOLATION",
1937
+ "PATH_SCOPE_VIOLATION",
1938
+ "RECOVERY_REQUIRED",
1939
+ "QUARANTINED",
1940
+ "PUBLICATION_LEASE_DRIFT",
1941
+ "PUBLICATION_REMOTE_DRIFT",
1942
+ "PUBLICATION_LEASE_EXPIRED",
1943
+ "INTEGRATION_INTENT_DRIFT",
1944
+ "INVALID_RETIREMENT_EXECUTION_RECORD",
1945
+ "RETIREMENT_REMOVAL_UNATTESTED",
1946
+ }
1947
+ failed = bool(result["errors"]) or any(
1948
+ warning.get("severity") == "blocking"
1949
+ or (warning.get("severity") is None and warning["code"] in failure_codes)
1950
+ for warning in result["warnings"]
1951
+ )
1952
+ degraded = any(warning.get("severity") == "degraded" for warning in result["warnings"])
1953
+ result["decision"] = {
1954
+ "code": "FAIL" if failed else "DEGRADED" if degraded else "PASS",
1955
+ "primary_diagnosis": _primary_diagnosis(
1956
+ diagnosis, result["warnings"] + result["errors"], failed=failed
1957
+ ),
1958
+ "reason": (
1959
+ "diagnostic findings require review"
1960
+ if failed
1961
+ else "non-blocking lifecycle findings remain"
1962
+ if degraded
1963
+ else "no blocking drift detected"
1964
+ ),
1965
+ "transactions": {
1966
+ "historical_terminal": historical_transactions,
1967
+ "unresolved_count": transaction_count,
1968
+ },
1969
+ "retirement_executions": {
1970
+ "outstanding": retirement_grants,
1971
+ "unattested_removals": retirement_unattested,
1972
+ },
1973
+ }
1974
+ return result