worktree-controller 1.32.1__tar.gz → 1.32.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/PKG-INFO +5 -5
  2. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/README.md +4 -4
  3. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/architecture.md +2 -2
  4. worktree_controller-1.32.2/docs/scope.md +86 -0
  5. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/pyproject.toml +1 -1
  6. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/pyproject.toml.orig +1 -1
  7. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/__init__.py +1 -1
  8. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/canonical_transition.py +2 -2
  9. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/controller.py +4 -20
  10. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/convergence.py +2 -2
  11. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/delivered_closeout.py +6 -6
  12. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/frozen_recovery.py +3 -3
  13. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/integration_conflict.py +4 -3
  14. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/integration_retry.py +10 -8
  15. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle.py +4 -64
  16. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/reconciler.py +3 -2
  17. worktree_controller-1.32.2/src/worktree_controller/record_updates.py +54 -0
  18. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/registration_prune.py +2 -2
  19. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/semantics.py +24 -0
  20. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/tool_cli_contract.json +1 -1
  21. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/tool_cli_contract.json +1 -1
  22. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/LICENSE +0 -0
  23. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/client-integration.md +0 -0
  24. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/daily-workflow.md +0 -0
  25. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/first-change.md +0 -0
  26. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/getting-started.md +0 -0
  27. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/operations.md +0 -0
  28. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/publishing.md +0 -0
  29. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/docs/reference.md +0 -0
  30. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/examples/change_task.py +0 -0
  31. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/examples/client_continuation.py +0 -0
  32. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/examples/quickstart.py +0 -0
  33. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/activation_layout.py +0 -0
  34. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/branch_retirement.py +0 -0
  35. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/canonical_refresh.py +0 -0
  36. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_attempts.py +0 -0
  37. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_evidence.py +0 -0
  38. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_recovery.py +0 -0
  39. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/cli.py +0 -0
  40. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/disposable.py +0 -0
  41. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/environment.py +0 -0
  42. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/git_backend.py +0 -0
  43. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/governance.py +0 -0
  44. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/hardlinks.py +0 -0
  45. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/integration_cleanup.py +0 -0
  46. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/keyed_document_scope.py +0 -0
  47. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle_history.py +0 -0
  48. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle_observation.py +0 -0
  49. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/metadata_codec.py +0 -0
  50. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/metadata_relocation.py +0 -0
  51. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/models.py +0 -0
  52. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/mount_diagnostics.py +0 -0
  53. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/multiple_cleanup_recovery.py +0 -0
  54. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/namespace_migration.py +0 -0
  55. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/native_session_recovery.py +0 -0
  56. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/owner_actions.py +0 -0
  57. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/policy.py +0 -0
  58. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/preservation.py +0 -0
  59. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/preserved_cleanup.py +0 -0
  60. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/publication_execution.py +0 -0
  61. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/recovery_guidance.py +0 -0
  62. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/registration_backfill.py +0 -0
  63. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/rendering.py +0 -0
  64. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/resource_admission.py +0 -0
  65. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/retirement_adapters.py +0 -0
  66. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/retirement_execution.py +0 -0
  67. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/session.py +0 -0
  68. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/session_admission.py +0 -0
  69. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/source_cleanup.py +0 -0
  70. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/source_preservation.py +0 -0
  71. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/source_recovery.py +0 -0
  72. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/store.py +0 -0
  73. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/task_workflow.py +0 -0
  74. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/transaction_scope.py +0 -0
  75. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/writer_lease.py +0 -0
  76. {worktree_controller-1.32.1 → worktree_controller-1.32.2}/src/worktree_controller/writer_session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: worktree-controller
3
- Version: 1.32.1
3
+ Version: 1.32.2
4
4
  Summary: Govern Git worktree budgets, authority, ownership, and lifecycle
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -23,7 +23,7 @@ It checks repository policy before allocating a worktree, grants writing authori
23
23
  process, and records handoff, integration, recovery and release evidence. Git remains the source
24
24
  of truth for files, branches, commits and registered worktrees.
25
25
 
26
- **Current release: 1.32.0 · Linux · Python 3.12 · Apache-2.0**
26
+ **Current release: 1.32.2 · Linux · Python 3.12 · Apache-2.0**
27
27
 
28
28
  ## Install
29
29
 
@@ -32,7 +32,7 @@ Use a virtual environment in a new directory:
32
32
  ```bash
33
33
  python3.12 -m venv .venv
34
34
  . .venv/bin/activate
35
- python -m pip install worktree-controller==1.32.0
35
+ python -m pip install worktree-controller==1.32.2
36
36
  worktree-controller --version
37
37
  worktree-controller capabilities --json
38
38
  ```
@@ -41,7 +41,7 @@ WC requires Git and readable Linux `/proc`. Other operating systems and Python v
41
41
  supported by this release. Its one runtime dependency is `websockets>=15,<16`.
42
42
  On Ubuntu, if creating the environment reports that `ensurepip` is unavailable, install the
43
43
  distribution's `python3.12-venv` package first (for example, `sudo apt install python3.12-venv`).
44
- Software version 1.32.0 uses CLI contract **61** and storage layout **5**.
44
+ Software version 1.32.2 uses CLI contract **61** and storage layout **5**.
45
45
 
46
46
  ## Try it in a disposable repository
47
47
 
@@ -50,7 +50,7 @@ With the environment above active:
50
50
  ```bash
51
51
  git clone https://github.com/xuanheng-tech/worktree-controller.git wc
52
52
  cd wc
53
- git checkout v1.32.0
53
+ git checkout v1.32.2
54
54
  python examples/quickstart.py --workspace "$HOME/wc-first-run"
55
55
  ```
56
56
 
@@ -6,7 +6,7 @@ It checks repository policy before allocating a worktree, grants writing authori
6
6
  process, and records handoff, integration, recovery and release evidence. Git remains the source
7
7
  of truth for files, branches, commits and registered worktrees.
8
8
 
9
- **Current release: 1.32.0 · Linux · Python 3.12 · Apache-2.0**
9
+ **Current release: 1.32.2 · Linux · Python 3.12 · Apache-2.0**
10
10
 
11
11
  ## Install
12
12
 
@@ -15,7 +15,7 @@ Use a virtual environment in a new directory:
15
15
  ```bash
16
16
  python3.12 -m venv .venv
17
17
  . .venv/bin/activate
18
- python -m pip install worktree-controller==1.32.0
18
+ python -m pip install worktree-controller==1.32.2
19
19
  worktree-controller --version
20
20
  worktree-controller capabilities --json
21
21
  ```
@@ -24,7 +24,7 @@ WC requires Git and readable Linux `/proc`. Other operating systems and Python v
24
24
  supported by this release. Its one runtime dependency is `websockets>=15,<16`.
25
25
  On Ubuntu, if creating the environment reports that `ensurepip` is unavailable, install the
26
26
  distribution's `python3.12-venv` package first (for example, `sudo apt install python3.12-venv`).
27
- Software version 1.32.0 uses CLI contract **61** and storage layout **5**.
27
+ Software version 1.32.2 uses CLI contract **61** and storage layout **5**.
28
28
 
29
29
  ## Try it in a disposable repository
30
30
 
@@ -33,7 +33,7 @@ With the environment above active:
33
33
  ```bash
34
34
  git clone https://github.com/xuanheng-tech/worktree-controller.git wc
35
35
  cd wc
36
- git checkout v1.32.0
36
+ git checkout v1.32.2
37
37
  python examples/quickstart.py --workspace "$HOME/wc-first-run"
38
38
  ```
39
39
 
@@ -10,8 +10,8 @@ development and recovery paths; exact public interfaces live in the [contract](.
10
10
  | --- | --- | --- |
11
11
  | CLI parsing and routing | `cli.py`, command modules | Dispatch explicit operations; do not duplicate authority decisions |
12
12
  | Git observations and mutations | `git_backend.py` | Git owns registrations, refs, paths, HEAD and cleanliness |
13
- | State and durable evidence | `models.py`, `store.py`, `metadata_codec.py` | Validate stored identities; serialize mutations under the repository lock |
14
- | Shared identity and semantic checks | `semantics.py` | Consume the caller's snapshot; do not acquire authority or write metadata |
13
+ | State and durable evidence | `models.py`, `store.py`, `metadata_codec.py`, `record_updates.py` | Validate stored identities; serialize mutations under the repository lock; record updates bump their version, and both update helpers append history |
14
+ | Shared identity and semantic checks | `semantics.py` | Consume the caller's snapshot, including effective intent state and terminal reconciliation; do not acquire authority or write metadata |
15
15
  | Budget admission | `resource_admission.py` | Preserve durable request identity and host reservations across retries |
16
16
  | Writer entitlement | `writer_lease.py`, `writer_session.py`, `governance.py` | Bind the real process identity and exact operation/target |
17
17
  | Lifecycle orchestration | `lifecycle.py` and focused recovery modules | Compose existing guards and append durable transition evidence |
@@ -0,0 +1,86 @@
1
+ # Scope and consumer surface (draft for owner decision)
2
+
3
+ This document records which parts of the public CLI are generic worktree governance and which
4
+ exist mainly for one consumer. It is an inventory plus proposed boundaries; nothing here changes
5
+ behavior, and every "proposed" line needs an owner decision before code moves.
6
+
7
+ Snapshot: CLI contract 61, 81 commands, 65 `*_version` capability keys. The public contract
8
+ counts were rechecked on 2026-10-23; consumer mappings below are the cloud audit
9
+ reported in the maintainer handoff, not a runtime trace or proof of exclusive use.
10
+
11
+ ## How the inventory was made
12
+
13
+ - **Agent Workspace (AW):** every `_invoke(...)` call in AW's controller client
14
+ (`backend/src/agent_workspace/infrastructure/worktree_controller.py`) plus its session-retirement
15
+ and external-execution modules. This was an exact static match in that audit, not a guarantee
16
+ that dynamic or other consumers do not exist.
17
+ - **Git Finalizer (GF):** word match over `git-finalize*` and `tooling/*.py`. Common words
18
+ (`review`, `task`, `release`, `resume`, `status`, `doctor`) can be false positives; the
19
+ retirement and publication entries are confirmed by GF's adapter code.
20
+ - **Docs/examples:** word match over `README.md`, `docs/`, `examples/` and the shipped Skill.
21
+
22
+ ## 1. Generic governance core (keep; any client)
23
+
24
+ Checkout budget, ownership and safe release for any person or agent sharing a repository:
25
+
26
+ `plan`, `status`, `doctor`, `capabilities`, `acquire`, `adopt`, `park`, `reopen`, `release`,
27
+ `release-intent`, `quarantine`, `review`, `handoff`, `resume`, `task`, `complete-no-change`,
28
+ `recovery-guide`, `lifecycle-history`, `write-guard`, `writer-lease-acquire`,
29
+ `writer-lease-release`, `writer-session`, `lease-acquire`, `lease-complete`, `lease-abort`,
30
+ `integration-enqueue`, `integration-claim`, `integration-validate`, `integration-queue`,
31
+ `integration-refresh`, `reconcile`.
32
+
33
+ ## 2. Delivery-tool protocols (keep; versioned, consumed by GF)
34
+
35
+ `branch-retirement-plan`, `-verify`, `-authorize`, `-complete`, `-record`, `-status`,
36
+ `-executions`, and `publication-execution-session`. These are the public protocols GF consumes
37
+ through capabilities; they should evolve by capability version, not by CLI contract number.
38
+
39
+ ## 3. Host-runtime integration (used by AW today)
40
+
41
+ | Command | Used by AW | Note |
42
+ | --- | --- | --- |
43
+ | `host-execution` | yes | Budgets a host process with no repository writer lease; default `--client-id` is `agent-workspace` |
44
+ | `execution-admit`, `execution-cancel` | yes | Durable admission of governed executions |
45
+ | `admission-queue` | yes | Periodic queue reconciliation |
46
+ | `binding-add`, `binding-remove`, `binding-transfer` | yes | Runtime bindings owned by a host |
47
+ | `lifecycle-events`, `lifecycle-event-ack` | yes | Event feed with consumer acknowledgement |
48
+ | `session-admit`, `session-bind`, `reconcile-session` | not found in AW's client | Need an owner answer: other host, legacy, or removable |
49
+
50
+ **Proposed:** keep these in WC only if they are documented as a generic *host runtime protocol*
51
+ (any host, explicit `--client-id`, no AW-specific defaults or wording). Otherwise move the
52
+ AW-specific policy (memory budgets, default identities) into AW and keep WC's part to admission
53
+ and binding records.
54
+
55
+ ## 4. Recovery and maintenance tail
56
+
57
+ `recover-handoff`, `recover-published`, `recovery-adopt`, `release-recovery-inspect`,
58
+ `integration-abort`, `-abandon`, `-reopen`, `-resolve`, `-retry`, `-equivalent`,
59
+ `-recovery-matrix`, `close-delivered`, `close-preserved`, `adopt-delivered`, `archive-retained`,
60
+ `externalize`, `resource-register`, `resource-clean`, `resource-recover`, `gc-plan`, `gc-apply`,
61
+ `reconcile-writers`, `acquire-across`, `acquire-when-free`, `canonical-refresh`,
62
+ `canonical-transition`, `namespace-migration-plan`, `-verify`, `-receipts`, `namespace-migrate`.
63
+
64
+ Most of these have no consumer in AW or GF and appear only in reference docs, or not at all.
65
+
66
+ **Proposed:** classify each as *supported*, *operator-only* or *deprecated* in the CLI contract,
67
+ publish a deprecation window for the last group, and stop adding a new top-level command per
68
+ recovery case — prefer one `recover` entry with typed subcases.
69
+
70
+ ## Open questions for the owner
71
+
72
+ 1. Is `host-execution` meant for hosts other than AW? If not, should its policy move to AW?
73
+ 2. Who uses `session-admit`, `session-bind` and `reconcile-session`?
74
+ 3. Which section-4 commands may be deprecated, and on what timeline?
75
+
76
+ ## Current compatibility decision
77
+
78
+ Keep every existing command supported while these ownership questions are unresolved. Do not
79
+ remove host commands, change default identities, relax recovery guards or deprecate commands
80
+ from an absence of static matches. New hosts use explicit client/session identities and the
81
+ versioned public contracts described in [client integration](client-integration.md). WC remains
82
+ the lifecycle and policy authority; consumers retain their business and release approvals.
83
+
84
+ Effective integration-state interpretation lives in `semantics.py`;
85
+ record and intent updates live in `record_updates.py`. These moves preserve CLI contract 61 and
86
+ storage layout 5. See [architecture](architecture.md) for module responsibilities.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "worktree-controller"
7
- version = "1.32.1"
7
+ version = "1.32.2"
8
8
  description = "Govern Git worktree budgets, authority, ownership, and lifecycle"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.13"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "worktree-controller"
7
- version = "1.32.1"
7
+ version = "1.32.2"
8
8
  description = "Govern Git worktree budgets, authority, ownership, and lifecycle"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12,<3.13"
@@ -1,3 +1,3 @@
1
1
  """Worktree lifecycle controller."""
2
2
 
3
- __version__ = "1.32.1"
3
+ __version__ = "1.32.2"
@@ -19,7 +19,6 @@ from .git_backend import (
19
19
  update_branch_ref,
20
20
  validate_branch_name,
21
21
  )
22
- from .lifecycle import _updated_record
23
22
  from .models import (
24
23
  ControllerError,
25
24
  MetadataRecord,
@@ -29,6 +28,7 @@ from .models import (
29
28
  WorktreePolicy,
30
29
  )
31
30
  from .policy import default_policy, load_policy, parse_policy
31
+ from .record_updates import updated_record
32
32
  from .semantics import managed_record as _managed_record
33
33
  from .store import MetadataStore
34
34
  from .transaction_scope import conflicts
@@ -274,7 +274,7 @@ def transition_canonical(
274
274
  if not refresh_main and item.branch == expected_branch:
275
275
  git_write(item.path, ("checkout", "--no-overwrite-ignore", expected_branch))
276
276
  raise
277
- updated = _updated_record(
277
+ updated = updated_record(
278
278
  record,
279
279
  "CANONICAL_MAIN_REFRESHED" if refresh_main else "CANONICAL_TRANSITIONED",
280
280
  lifecycle="ACTIVE",
@@ -33,9 +33,9 @@ from .mount_diagnostics import diagnose_mounts, mount_warnings
33
33
  from .policy import load_policy
34
34
  from .semantics import (
35
35
  EVIDENCE_LIFECYCLES,
36
+ effective_intent_state,
36
37
  evidence_head_issues,
37
38
  integration_candidate_issues,
38
- integration_intent_abandoned,
39
39
  integration_queue_order,
40
40
  normalize_scope_paths,
41
41
  path_scope_issues,
@@ -248,22 +248,6 @@ def _intent_for_allocation(snapshot: MetadataSnapshot, allocation_id: str | None
248
248
  return matches[0] if matches else None
249
249
 
250
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
251
  def _integration_intent_order(intent: Any) -> tuple[int, int | str, str]:
268
252
  if intent.queue_sequence > 0:
269
253
  return (1, intent.queue_sequence, intent.intent_id)
@@ -400,7 +384,7 @@ def _repository_payload(
400
384
  (
401
385
  intent
402
386
  for intent in snapshot.integration_intents.values()
403
- if _effective_intent_state(snapshot, intent) in {"QUEUED", "WAITING_SCOPE"}
387
+ if effective_intent_state(snapshot, intent) in {"QUEUED", "WAITING_SCOPE"}
404
388
  ),
405
389
  key=lambda value: integration_queue_order(snapshot, value),
406
390
  )
@@ -543,7 +527,7 @@ def _worktree_payload(
543
527
  "source_allocations": list(record.source_allocations) if record else [],
544
528
  "integration_intent_id": intent.intent_id if intent else None,
545
529
  "integration_intent_state": (
546
- _effective_intent_state(snapshot, intent)
530
+ effective_intent_state(snapshot, intent)
547
531
  if intent is not None and snapshot is not None
548
532
  else None
549
533
  ),
@@ -1317,7 +1301,7 @@ def _semantic_warnings(
1317
1301
  "RELEASED": {"RELEASED"},
1318
1302
  }
1319
1303
  for intent in snapshot.integration_intents.values():
1320
- effective_state = _effective_intent_state(snapshot, intent)
1304
+ effective_state = effective_intent_state(snapshot, intent)
1321
1305
  if effective_state in {"QUEUED", "WAITING_SCOPE", "ABANDONED"}:
1322
1306
  continue
1323
1307
  if unallocated_integration_conflict(intent):
@@ -199,13 +199,13 @@ def _is_candidate(record: MetadataRecord) -> bool:
199
199
 
200
200
 
201
201
  def _integration_bound(snapshot: MetadataSnapshot, record: MetadataRecord) -> bool:
202
- from .lifecycle import _effective_intent_state
202
+ from .semantics import effective_intent_state
203
203
 
204
204
  return any(
205
205
  intent.allocation_id == record.allocation_id
206
206
  or record.allocation_id in intent.source_allocations
207
207
  for intent in snapshot.integration_intents.values()
208
- if _effective_intent_state(snapshot, intent) not in TERMINAL_INTEGRATION_STATES
208
+ if effective_intent_state(snapshot, intent) not in TERMINAL_INTEGRATION_STATES
209
209
  ) or any(
210
210
  other.lifecycle != "RELEASED" and record.allocation_id in other.source_allocations
211
211
  for other in snapshot.records.values()
@@ -345,11 +345,11 @@ def _eligible(
345
345
  for_typed_cleanup: bool = False,
346
346
  ) -> dict[str, str]:
347
347
  from .lifecycle import (
348
- _effective_intent_state,
349
348
  _typed_runtime_blockers,
350
349
  _typed_runtime_evidence,
351
350
  _validate_release_environment,
352
351
  )
352
+ from .semantics import effective_intent_state
353
353
 
354
354
  if for_typed_cleanup and (state != "RETAINED" or require_publication):
355
355
  raise ControllerError("INVALID_LIFECYCLE", "typed cleanup does not authorize publication")
@@ -410,7 +410,7 @@ def _eligible(
410
410
  record.allocation_id in intent.source_allocations
411
411
  and (
412
412
  not (
413
- (effective_state := _effective_intent_state(snapshot, intent)) == "ABANDONED"
413
+ (effective_state := effective_intent_state(snapshot, intent)) == "ABANDONED"
414
414
  or (not require_publication and effective_state == "RELEASED")
415
415
  )
416
416
  or not is_ancestor(
@@ -662,7 +662,7 @@ def close_delivered(
662
662
  plan_id: str | None = None,
663
663
  ) -> dict[str, Any]:
664
664
  from .controller import envelope
665
- from .lifecycle import _updated_record
665
+ from .record_updates import updated_record
666
666
  from .semantics import assert_record_version as _cas
667
667
  from .semantics import managed_record as _managed_record
668
668
 
@@ -758,7 +758,7 @@ def close_delivered(
758
758
  "plan_id": identity,
759
759
  "plan": plan,
760
760
  }
761
- updated = _updated_record(record, EVENT, lifecycle=STATE)
761
+ updated = updated_record(record, EVENT, lifecycle=STATE)
762
762
  event = {
763
763
  **updated.history[-1],
764
764
  "closeout_receipt_id": receipt_id,
@@ -803,7 +803,7 @@ def archive_retained(
803
803
  ) -> dict[str, Any]:
804
804
  """Freeze a clean, unfinished retained task for guarded physical release."""
805
805
  from .controller import envelope
806
- from .lifecycle import _updated_record
806
+ from .record_updates import updated_record
807
807
  from .semantics import assert_record_version as _cas
808
808
  from .semantics import managed_record as _managed_record
809
809
 
@@ -896,7 +896,7 @@ def archive_retained(
896
896
  "plan_id": identity,
897
897
  "plan": plan,
898
898
  }
899
- updated = _updated_record(record, ARCHIVE_EVENT, lifecycle=ARCHIVE_STATE)
899
+ updated = updated_record(record, ARCHIVE_EVENT, lifecycle=ARCHIVE_STATE)
900
900
  event = {
901
901
  **updated.history[-1],
902
902
  "archive_receipt_id": receipt_id,
@@ -20,10 +20,10 @@ from .lifecycle import (
20
20
  _cancel_release_intents,
21
21
  _typed_runtime_blockers,
22
22
  _typed_runtime_evidence,
23
- _updated_record,
24
23
  )
25
24
  from .models import ControllerError
26
25
  from .policy import load_policy
26
+ from .record_updates import updated_record
27
27
  from .semantics import assert_record_version as _cas
28
28
  from .semantics import integration_intent_abandoned, publication_ref
29
29
  from .semantics import managed_record as _managed_record
@@ -245,7 +245,7 @@ def recover_frozen_checkout(
245
245
  git_write(item.path, ("symbolic-ref", "HEAD", f"refs/heads/{branch}"))
246
246
  elif latest.branch != branch:
247
247
  raise ControllerError("BLOCK_DRIFT", "checkout branch changed during recovery")
248
- updated = _updated_record(record, "PUBLISHED_CHECKOUT_REATTACHED")
248
+ updated = updated_record(record, "PUBLISHED_CHECKOUT_REATTACHED")
249
249
  else:
250
250
  _cancel_release_intents(
251
251
  store,
@@ -254,7 +254,7 @@ def recover_frozen_checkout(
254
254
  event="FROZEN_SOURCE_RECOVERED",
255
255
  blocker="REOPENED",
256
256
  )
257
- updated = _updated_record(
257
+ updated = updated_record(
258
258
  record,
259
259
  "PUBLISHED_SOURCE_RECOVERED",
260
260
  lifecycle="ACTIVE",
@@ -23,7 +23,8 @@ def resolve_integration(
23
23
  finalizer_record_id: str,
24
24
  ) -> dict[str, Any]:
25
25
  from .controller import status
26
- from .lifecycle import _intent_for_allocation, _intent_payload, _updated_intent, _updated_record
26
+ from .lifecycle import _intent_for_allocation, _intent_payload
27
+ from .record_updates import updated_intent, updated_record
27
28
  from .semantics import assert_record_version as _cas
28
29
  from .semantics import managed_record as _managed_record
29
30
 
@@ -84,8 +85,8 @@ def resolve_integration(
84
85
  issues = integration_candidate_issues(item, record, snapshot, require_task_match=False)
85
86
  if issues:
86
87
  raise ControllerError(issues[0]["code"], issues[0]["message"])
87
- updated = _updated_record(record, "INTEGRATION_CONFLICT_RESOLVED", task_commit=item.head)
88
- resolved = _updated_intent(
88
+ updated = updated_record(record, "INTEGRATION_CONFLICT_RESOLVED", task_commit=item.head)
89
+ resolved = updated_intent(
89
90
  intent,
90
91
  "INTEGRATION_CONFLICT_RESOLVED",
91
92
  candidate_oid=item.head,
@@ -227,7 +227,8 @@ def _prerequisite(
227
227
  parent: IntegrationIntent,
228
228
  child: IntegrationIntent,
229
229
  ) -> dict[str, Any]:
230
- from .lifecycle import _effective_intent_state, _scopes_overlap
230
+ from .lifecycle import _scopes_overlap
231
+ from .semantics import effective_intent_state
231
232
 
232
233
  if (
233
234
  child.owner != parent.owner
@@ -255,7 +256,7 @@ def _prerequisite(
255
256
  if any(
256
257
  other.intent_id != parent.intent_id
257
258
  and other.allocation_id is not None
258
- and _effective_intent_state(snapshot, other) not in {"RELEASED", "ABANDONED"}
259
+ and effective_intent_state(snapshot, other) not in {"RELEASED", "ABANDONED"}
259
260
  and other.queue_sequence > parent.queue_sequence
260
261
  and _scopes_overlap(other.scope_paths, child.scope_paths)
261
262
  for other in snapshot.integration_intents.values()
@@ -295,7 +296,7 @@ def _plan(
295
296
  prerequisite_id: str | None,
296
297
  reason: str,
297
298
  ) -> dict[str, Any]:
298
- from .lifecycle import _effective_intent_state
299
+ from .semantics import effective_intent_state
299
300
 
300
301
  failure = _failure(repository, snapshot, store, intent)
301
302
  source_records = _sources(repository, snapshot, store, intent)
@@ -324,7 +325,7 @@ def _plan(
324
325
  bound = integration_prerequisite(intent)
325
326
  if bound:
326
327
  child = snapshot.integration_intents.get(bound)
327
- if child is None or _effective_intent_state(snapshot, child) != "RELEASED":
328
+ if child is None or effective_intent_state(snapshot, child) != "RELEASED":
328
329
  raise ControllerError(
329
330
  "RETRY_PREREQUISITE_PENDING",
330
331
  "prerequisite needs formal publication and release",
@@ -379,7 +380,7 @@ def _plan(
379
380
  "queue_evidence": {
380
381
  other.intent_id: _digest(integration_intent_to_dict(other))
381
382
  for other in snapshot.integration_intents.values()
382
- if _effective_intent_state(snapshot, other) not in {"RELEASED", "ABANDONED"}
383
+ if effective_intent_state(snapshot, other) not in {"RELEASED", "ABANDONED"}
383
384
  },
384
385
  }
385
386
  return {**value, "plan_id": _digest(value)}
@@ -397,7 +398,8 @@ def retry_integration(
397
398
  ) -> dict[str, Any]:
398
399
  """Plan/CAS an append-only dependency decision or restart the original intent."""
399
400
  from .controller import status
400
- from .lifecycle import _blocking_scope_intent, _emit_lifecycle_event, _updated_intent
401
+ from .lifecycle import _blocking_scope_intent, _emit_lifecycle_event
402
+ from .record_updates import updated_intent
401
403
 
402
404
  detail = reason.strip()
403
405
  if not detail or len(detail) > 512 or "\x00" in detail:
@@ -451,9 +453,9 @@ def retry_integration(
451
453
  raise ControllerError("BLOCK_DRIFT", "retry plan evidence changed; plan again")
452
454
  if not dry_run:
453
455
  if prerequisite_intent_id:
454
- updated = _updated_intent(intent, "RETRY_PREREQUISITE_BOUND")
456
+ updated = updated_intent(intent, "RETRY_PREREQUISITE_BOUND")
455
457
  else:
456
- updated = _updated_intent(
458
+ updated = updated_intent(
457
459
  intent,
458
460
  "PREPARATION_RETRY_QUEUED",
459
461
  state="QUEUED",
@@ -77,6 +77,8 @@ from .models import (
77
77
  from .policy import load_policy
78
78
  from .preserved_cleanup import CONTRACT as PRESERVED_CLEANUP_CONTRACT
79
79
  from .preserved_cleanup import coverage, delete_preserved_state
80
+ from .record_updates import updated_intent as _updated_intent
81
+ from .record_updates import updated_record as _updated_record
80
82
  from .semantics import assert_record_version as _cas
81
83
  from .semantics import (
82
84
  attribution_base,
@@ -90,6 +92,8 @@ from .semantics import (
90
92
  path_scope_issues,
91
93
  publication_ref,
92
94
  )
95
+ from .semantics import effective_intent_state as _effective_intent_state
96
+ from .semantics import integration_reconciliation as _integration_reconciliation
93
97
  from .semantics import managed_record as _managed_record
94
98
  from .semantics import record_for as _record_for
95
99
  from .store import (
@@ -106,11 +110,9 @@ from .store import (
106
110
  RELEASE_RECEIPT_SCHEMA_VERSION,
107
111
  MetadataStore,
108
112
  integration_intent_from_dict,
109
- integration_intent_to_dict,
110
113
  lifecycle_event_from_dict,
111
114
  new_record,
112
115
  now_iso,
113
- record_from_dict,
114
116
  record_to_dict,
115
117
  release_intent_from_dict,
116
118
  release_intent_to_dict,
@@ -146,46 +148,6 @@ def _crash_point(name: str) -> None:
146
148
  os._exit(97)
147
149
 
148
150
 
149
- def _updated_record(record: MetadataRecord, event: str, **changes: Any) -> MetadataRecord:
150
- payload = record_to_dict(record)
151
- before = record.lifecycle
152
- timestamp = now_iso()
153
- payload.update(changes)
154
- payload["record_version"] = record.record_version + 1
155
- payload["last_reviewed_at"] = timestamp
156
- history = list(record.history)
157
- history.append(
158
- {
159
- "at": timestamp,
160
- "event": event,
161
- "from": before,
162
- "to": payload.get("lifecycle", before),
163
- "owner": payload.get("owner", record.owner),
164
- }
165
- )
166
- payload["history"] = history
167
- return record_from_dict(payload)
168
-
169
-
170
- def _updated_intent(intent: IntegrationIntent, event: str, **changes: Any) -> IntegrationIntent:
171
- payload = integration_intent_to_dict(intent)
172
- timestamp = now_iso()
173
- payload.update(changes)
174
- payload["updated_at"] = timestamp
175
- history = list(intent.history)
176
- history.append(
177
- {
178
- "at": timestamp,
179
- "event": event,
180
- "from": intent.state,
181
- "to": payload.get("state", intent.state),
182
- "candidate_oid": payload.get("candidate_oid"),
183
- }
184
- )
185
- payload["history"] = history
186
- return integration_intent_from_dict(payload)
187
-
188
-
189
151
  def _scope_digest(scope_paths: tuple[str, ...]) -> str:
190
152
  encoded = json.dumps(list(scope_paths), ensure_ascii=True, separators=(",", ":"))
191
153
  return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
@@ -211,28 +173,6 @@ def _scopes_overlap(left: tuple[str, ...], right: tuple[str, ...]) -> bool:
211
173
  return False
212
174
 
213
175
 
214
- def _integration_reconciliation(
215
- snapshot: MetadataSnapshot, intent_id: str
216
- ) -> IntegrationReconciliationReceipt | None:
217
- matches = [
218
- receipt
219
- for receipt in snapshot.integration_reconciliation_receipts.values()
220
- if receipt.intent_id == intent_id and receipt.result == "STALE_INTENT_TERMINAL_ACKNOWLEDGED"
221
- ]
222
- if len(matches) > 1:
223
- raise ControllerError(
224
- "INVALID_METADATA", "integration intent has multiple terminal reconciliations"
225
- )
226
- return matches[0] if matches else None
227
-
228
-
229
- def _effective_intent_state(snapshot: MetadataSnapshot, intent: IntegrationIntent) -> str:
230
- receipt = _integration_reconciliation(snapshot, intent.intent_id)
231
- if receipt is not None:
232
- return receipt.effective_terminal_state
233
- return "ABANDONED" if integration_intent_abandoned(intent) else intent.state
234
-
235
-
236
176
  def _released_intent_evidence(
237
177
  snapshot: MetadataSnapshot,
238
178
  intent: IntegrationIntent,
@@ -20,7 +20,7 @@ from typing import Any
20
20
 
21
21
  from .controller import status
22
22
  from .git_backend import inventory
23
- from .lifecycle import _cancel_release_intents, _updated_record
23
+ from .lifecycle import _cancel_release_intents
24
24
  from .models import (
25
25
  ControllerError,
26
26
  MetadataRecord,
@@ -28,6 +28,7 @@ from .models import (
28
28
  WriterReconciliationReceipt,
29
29
  )
30
30
  from .policy import load_policy
31
+ from .record_updates import updated_record
31
32
  from .semantics import record_for as _record_for
32
33
  from .store import WRITER_RECEIPT_SCHEMA_VERSION, MetadataStore
33
34
  from .writer_lease import (
@@ -278,7 +279,7 @@ def reconcile_writers(
278
279
  event="RECORD_CHANGED",
279
280
  blocker="PARKED",
280
281
  )
281
- updated = _updated_record(record, "PARKED", lifecycle="RETAINED")
282
+ updated = updated_record(record, "PARKED", lifecycle="RETAINED")
282
283
  store.replace_record(updated)
283
284
  store.remove_writer_lease(record.allocation_id)
284
285
  receipt = WriterReconciliationReceipt(
@@ -0,0 +1,54 @@
1
+ """Versioned, history-preserving updates of allocation records and integration intents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .metadata_codec import (
8
+ integration_intent_from_dict,
9
+ integration_intent_to_dict,
10
+ record_from_dict,
11
+ record_to_dict,
12
+ )
13
+ from .models import IntegrationIntent, MetadataRecord
14
+ from .store import now_iso
15
+
16
+
17
+ def updated_record(record: MetadataRecord, event: str, **changes: Any) -> MetadataRecord:
18
+ payload = record_to_dict(record)
19
+ before = record.lifecycle
20
+ timestamp = now_iso()
21
+ payload.update(changes)
22
+ payload["record_version"] = record.record_version + 1
23
+ payload["last_reviewed_at"] = timestamp
24
+ history = list(record.history)
25
+ history.append(
26
+ {
27
+ "at": timestamp,
28
+ "event": event,
29
+ "from": before,
30
+ "to": payload.get("lifecycle", before),
31
+ "owner": payload.get("owner", record.owner),
32
+ }
33
+ )
34
+ payload["history"] = history
35
+ return record_from_dict(payload)
36
+
37
+
38
+ def updated_intent(intent: IntegrationIntent, event: str, **changes: Any) -> IntegrationIntent:
39
+ payload = integration_intent_to_dict(intent)
40
+ timestamp = now_iso()
41
+ payload.update(changes)
42
+ payload["updated_at"] = timestamp
43
+ history = list(intent.history)
44
+ history.append(
45
+ {
46
+ "at": timestamp,
47
+ "event": event,
48
+ "from": intent.state,
49
+ "to": payload.get("state", intent.state),
50
+ "candidate_oid": payload.get("candidate_oid"),
51
+ }
52
+ )
53
+ payload["history"] = history
54
+ return integration_intent_from_dict(payload)
@@ -20,7 +20,6 @@ from .git_backend import (
20
20
  public_refs,
21
21
  worktree_content_fingerprint,
22
22
  )
23
- from .lifecycle import _updated_record
24
23
  from .models import (
25
24
  ControllerError,
26
25
  MetadataRecord,
@@ -29,6 +28,7 @@ from .models import (
29
28
  WorktreeGitState,
30
29
  WorktreePolicy,
31
30
  )
31
+ from .record_updates import updated_record
32
32
  from .store import MetadataStore, now_iso
33
33
 
34
34
  CONTRACT = "registration-prune/v1"
@@ -343,7 +343,7 @@ def apply_prune(
343
343
  snapshot = store._load_unlocked()
344
344
  record = snapshot.records[journal["allocation_id"]]
345
345
  if record.record_version == journal["expected_record_version"]:
346
- updated = _updated_record(
346
+ updated = updated_record(
347
347
  record, "REGISTRATION_PRUNED", lifecycle="RELEASED", owner="unassigned"
348
348
  )
349
349
  store.replace_record(updated)
@@ -17,6 +17,7 @@ from .git_backend import (
17
17
  from .models import (
18
18
  ControllerError,
19
19
  IntegrationIntent,
20
+ IntegrationReconciliationReceipt,
20
21
  MetadataRecord,
21
22
  MetadataSnapshot,
22
23
  RepositoryInventory,
@@ -79,6 +80,29 @@ def integration_intent_abandoned(intent: IntegrationIntent) -> bool:
79
80
  )
80
81
 
81
82
 
83
+ def integration_reconciliation(
84
+ snapshot: MetadataSnapshot, intent_id: str
85
+ ) -> IntegrationReconciliationReceipt | None:
86
+ matches = [
87
+ receipt
88
+ for receipt in snapshot.integration_reconciliation_receipts.values()
89
+ if receipt.intent_id == intent_id and receipt.result == "STALE_INTENT_TERMINAL_ACKNOWLEDGED"
90
+ ]
91
+ if len(matches) > 1:
92
+ raise ControllerError(
93
+ "INVALID_METADATA", "integration intent has multiple terminal reconciliations"
94
+ )
95
+ return matches[0] if matches else None
96
+
97
+
98
+ def effective_intent_state(snapshot: MetadataSnapshot, intent: IntegrationIntent) -> str:
99
+ """Interpret terminal evidence from the caller's snapshot without changing authority."""
100
+ receipt = integration_reconciliation(snapshot, intent.intent_id)
101
+ if receipt is not None:
102
+ return receipt.effective_terminal_state
103
+ return "ABANDONED" if integration_intent_abandoned(intent) else intent.state
104
+
105
+
82
106
  def _current_attempt(intent: IntegrationIntent) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
83
107
  """Split history at the last formal retry: (earlier attempts, current attempt)."""
84
108
  history = list(intent.history)
@@ -1071,7 +1071,7 @@
1071
1071
  "session_launcher_entrypoint": "worktree-session",
1072
1072
  "session_entrypoint": "worktree-session-end",
1073
1073
  "tool_name": "worktree-controller",
1074
- "tool_version": "1.32.1",
1074
+ "tool_version": "1.32.2",
1075
1075
  "delivered_closeout_version": 1,
1076
1076
  "retained_archive_version": 1,
1077
1077
  "retention_convergence_version": 2,
@@ -1071,7 +1071,7 @@
1071
1071
  "session_launcher_entrypoint": "worktree-session",
1072
1072
  "session_entrypoint": "worktree-session-end",
1073
1073
  "tool_name": "worktree-controller",
1074
- "tool_version": "1.32.1",
1074
+ "tool_version": "1.32.2",
1075
1075
  "delivered_closeout_version": 1,
1076
1076
  "retained_archive_version": 1,
1077
1077
  "retention_convergence_version": 2,