worktree-controller 1.32.0__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.0 → worktree_controller-1.32.2}/PKG-INFO +5 -5
  2. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/README.md +4 -4
  3. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/architecture.md +2 -2
  4. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/client-integration.md +27 -0
  5. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/reference.md +4 -3
  6. worktree_controller-1.32.2/docs/scope.md +86 -0
  7. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/pyproject.toml +1 -1
  8. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/pyproject.toml.orig +1 -1
  9. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/__init__.py +1 -1
  10. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/canonical_transition.py +2 -2
  11. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/controller.py +4 -20
  12. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/convergence.py +2 -2
  13. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/delivered_closeout.py +6 -6
  14. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/frozen_recovery.py +3 -3
  15. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/integration_conflict.py +4 -3
  16. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/integration_retry.py +10 -8
  17. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/keyed_document_scope.py +16 -10
  18. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle.py +4 -64
  19. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/reconciler.py +3 -2
  20. worktree_controller-1.32.2/src/worktree_controller/record_updates.py +54 -0
  21. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/registration_prune.py +2 -2
  22. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/semantics.py +24 -0
  23. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/tool_cli_contract.json +2 -2
  24. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/tool_cli_contract.json +2 -2
  25. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/LICENSE +0 -0
  26. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/daily-workflow.md +0 -0
  27. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/first-change.md +0 -0
  28. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/getting-started.md +0 -0
  29. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/operations.md +0 -0
  30. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/publishing.md +0 -0
  31. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/examples/change_task.py +0 -0
  32. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/examples/client_continuation.py +0 -0
  33. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/examples/quickstart.py +0 -0
  34. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/activation_layout.py +0 -0
  35. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/branch_retirement.py +0 -0
  36. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/canonical_refresh.py +0 -0
  37. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_attempts.py +0 -0
  38. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_evidence.py +0 -0
  39. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_recovery.py +0 -0
  40. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cli.py +0 -0
  41. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/disposable.py +0 -0
  42. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/environment.py +0 -0
  43. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/git_backend.py +0 -0
  44. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/governance.py +0 -0
  45. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/hardlinks.py +0 -0
  46. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/integration_cleanup.py +0 -0
  47. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle_history.py +0 -0
  48. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle_observation.py +0 -0
  49. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/metadata_codec.py +0 -0
  50. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/metadata_relocation.py +0 -0
  51. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/models.py +0 -0
  52. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/mount_diagnostics.py +0 -0
  53. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/multiple_cleanup_recovery.py +0 -0
  54. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/namespace_migration.py +0 -0
  55. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/native_session_recovery.py +0 -0
  56. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/owner_actions.py +0 -0
  57. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/policy.py +0 -0
  58. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/preservation.py +0 -0
  59. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/preserved_cleanup.py +0 -0
  60. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/publication_execution.py +0 -0
  61. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/recovery_guidance.py +0 -0
  62. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/registration_backfill.py +0 -0
  63. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/rendering.py +0 -0
  64. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/resource_admission.py +0 -0
  65. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/retirement_adapters.py +0 -0
  66. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/retirement_execution.py +0 -0
  67. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/session.py +0 -0
  68. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/session_admission.py +0 -0
  69. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_cleanup.py +0 -0
  70. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_preservation.py +0 -0
  71. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_recovery.py +0 -0
  72. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/store.py +0 -0
  73. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/task_workflow.py +0 -0
  74. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/transaction_scope.py +0 -0
  75. {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/writer_lease.py +0 -0
  76. {worktree_controller-1.32.0 → 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.0
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 |
@@ -105,6 +105,33 @@ This demonstrates two CLI consumers, not actual invocation or discovery by two A
105
105
  Vendor-specific discovery and live-client use require their own observed acceptance, as described
106
106
  in [checks and deployment evidence](operations.md#checks-and-deployment).
107
107
 
108
+ ## Accept a real AI client
109
+
110
+ Accept a client by observing it use the installed WC interface during a real task,
111
+ including a pause and continuation in a later native session. Running the offline
112
+ protocol example checks the CLI workflow; it does not establish that an AI client
113
+ discovers or invokes WC correctly. Keep the actual session identities and checkpoint
114
+ evidence with the acceptance result, and limit claims to the client and workflow observed.
115
+
116
+ Use the separate `worktree-session-end` executable for session closeout. Keep a practical
117
+ evidence checklist:
118
+
119
+ - Capture actual native-client WC invocations, raw results and exit codes, client version,
120
+ and distinct native session/run/holder identities before and after interruption.
121
+ - Verify the raw checkpoint digest and unchanged Task/authority/owner, allocation, path,
122
+ branch, HEAD and scope; compare exact file SHA256 values before the next session edits.
123
+ - Resume the original Task, acquire a fresh live writer, and pass its check and write guard;
124
+ record refusal of the previous identity without reviving it or bypassing a refusal.
125
+ - While leases are recorded, check and end with the exact client/session/run/holder selector.
126
+ Include every allocation via `--expected-allocation-id`; retain every per-tree decision
127
+ and require `summary.terminal_complete` with `--require-complete` before claiming closeout.
128
+ - Follow the [first-change workflow](first-change.md) to finish; keep separate Finalizer,
129
+ handoff, integration/publication and release evidence before claiming task completion.
130
+ A retained checkpoint preserves unfinished work; it is not delivery evidence.
131
+
132
+ One tested vendor does not prove another vendor's discovery or invocation of WC; accept each
133
+ client and workflow using its own observed evidence.
134
+
108
135
  ## Upgrade an existing managed installation
109
136
 
110
137
  A public package installation in a new environment is separate from activation of a shared
@@ -1478,9 +1478,10 @@ checkout rejects feature writes. `READY_TO_PUBLISH` is write-fenced until explic
1478
1478
  out-of-band HEAD, content, record, or binding drift makes old evidence unusable.
1479
1479
 
1480
1480
  The bounded `integration-keyed-document-scope/v1` refinement applies only when the entire
1481
- overlap is the exact `docs/handbook/business-progress.json` path. Every pair of frozen source
1482
- commits must have one non-ancestor merge base and the same committed `.gitattributes` declaring
1483
- `merge=qp-handbook-progress`. QP schema 2 records are compared by whole `tasks.authority_key`
1481
+ overlap is exactly `docs/nexus/business-progress.json` with `merge=qp-nexus-progress`, or
1482
+ the historical `docs/handbook/business-progress.json` with `merge=qp-handbook-progress`.
1483
+ Cross-path moves retain the ordinary file fence. Every pair of frozen source commits must have
1484
+ one non-ancestor merge base and the same committed `.gitattributes` declaring the matching profile. QP schema 2 records are compared by whole `tasks.authority_key`
1484
1485
  or `development_lines/modules/decisions.id`, including additions and deletions. All changed key
1485
1486
  sets must be disjoint. Metadata, schema, ordering, duplicate keys, missing/oversized/non-regular
1486
1487
  blobs, unknown evidence, broad paths and code overlaps keep the ordinary file fence. Explicit
@@ -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.0"
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.0"
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.0"
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",
@@ -1,4 +1,4 @@
1
- """Prove independent Handbook records from frozen Git objects, never working files.
1
+ """Prove independent Nexus records from frozen Git objects, never working files.
2
2
 
3
3
  This refines only the known keyed-document overlap. It does not choose merge results,
4
4
  change declared ownership, or replace candidate validation and main publication CAS.
@@ -13,8 +13,11 @@ from pathlib import Path
13
13
  from .git_backend import _git
14
14
  from .models import ControllerError
15
15
 
16
- PROGRESS_PATH = "docs/handbook/business-progress.json"
17
- _ATTRIBUTE = f"{PROGRESS_PATH} merge=qp-handbook-progress"
16
+ PROGRESS_PATH = "docs/nexus/business-progress.json"
17
+ PROGRESS_PROFILES = {
18
+ PROGRESS_PATH: "qp-nexus-progress",
19
+ "docs/handbook/business-progress.json": "qp-handbook-progress",
20
+ }
18
21
  _COLLECTIONS = {
19
22
  "tasks": "authority_key",
20
23
  "development_lines": "id",
@@ -105,11 +108,11 @@ def _blob(repo: Path, commit: str, path: str, *, limit: int = _MAX_BYTES) -> byt
105
108
  return raw
106
109
 
107
110
 
108
- def _progress(repo: Path, commit: str) -> tuple[str, bytes]:
111
+ def _progress(repo: Path, commit: str, path: str) -> tuple[str, bytes]:
109
112
  attributes = _blob(repo, commit, ".gitattributes", limit=64 * 1024).decode("utf-8")
110
- if _ATTRIBUTE not in attributes.splitlines():
113
+ if f"{path} merge={PROGRESS_PROFILES[path]}" not in attributes.splitlines():
111
114
  raise ValueError("keyed merge protocol is not declared")
112
- return attributes, _blob(repo, commit, PROGRESS_PATH)
115
+ return attributes, _blob(repo, commit, path)
113
116
 
114
117
 
115
118
  def independent_progress_scopes(
@@ -133,7 +136,10 @@ def independent_progress_scopes(
133
136
  for right in right_paths
134
137
  if left == right or left.startswith(right + "/") or right.startswith(left + "/")
135
138
  }
136
- if overlaps != {(PROGRESS_PATH, PROGRESS_PATH)}:
139
+ if len(overlaps) != 1:
140
+ return False
141
+ path, other = next(iter(overlaps))
142
+ if path != other or path not in PROGRESS_PROFILES:
137
143
  return False
138
144
  if len(left_sources) > 8 or len(right_sources) > 8:
139
145
  return False
@@ -148,9 +154,9 @@ def independent_progress_scopes(
148
154
  base = bases[0].decode("ascii")
149
155
  if not _OID.fullmatch(base) or base in (left, right):
150
156
  return False
151
- attributes, original = _progress(repo, base)
152
- left_attributes, left_document = _progress(repo, left)
153
- right_attributes, right_document = _progress(repo, right)
157
+ attributes, original = _progress(repo, base, path)
158
+ left_attributes, left_document = _progress(repo, left, path)
159
+ right_attributes, right_document = _progress(repo, right, path)
154
160
  if attributes != left_attributes or attributes != right_attributes:
155
161
  return False
156
162
  left_keys = _changed_keys(original, left_document)
@@ -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)
@@ -468,7 +468,7 @@
468
468
  "contract": "integration-keyed-document-scope/v1",
469
469
  "entry_points": "normal enqueue, claim, reconcile and retry scope checks",
470
470
  "identity": "original frozen source commits, scopes and queue sequence; no mutable checkout evidence",
471
- "supported_document": "docs/handbook/business-progress.json, quant-platform schema 2, unchanged committed qp-handbook-progress merge profile",
471
+ "supported_document": "docs/nexus/business-progress.json with qp-nexus-progress, or historical docs/handbook/business-progress.json with qp-handbook-progress; quant-platform schema 2, unchanged committed merge profile",
472
472
  "proof": "unique non-ancestor merge base for every frozen source pair; whole-record changed keys must be disjoint across tasks.authority_key and development_lines/modules/decisions.id",
473
473
  "bounds": "at most 8 sources per intent, 2 MiB regular document blobs, 64 KiB attributes blobs",
474
474
  "fail_closed": "shared record, metadata/schema/order changes, broad paths, code overlap, duplicate keys, invalid/missing/oversized Git evidence, ancestry dependency, explicit prerequisite or earlier publishing/recovery/attention state",
@@ -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.0",
1074
+ "tool_version": "1.32.2",
1075
1075
  "delivered_closeout_version": 1,
1076
1076
  "retained_archive_version": 1,
1077
1077
  "retention_convergence_version": 2,
@@ -468,7 +468,7 @@
468
468
  "contract": "integration-keyed-document-scope/v1",
469
469
  "entry_points": "normal enqueue, claim, reconcile and retry scope checks",
470
470
  "identity": "original frozen source commits, scopes and queue sequence; no mutable checkout evidence",
471
- "supported_document": "docs/handbook/business-progress.json, quant-platform schema 2, unchanged committed qp-handbook-progress merge profile",
471
+ "supported_document": "docs/nexus/business-progress.json with qp-nexus-progress, or historical docs/handbook/business-progress.json with qp-handbook-progress; quant-platform schema 2, unchanged committed merge profile",
472
472
  "proof": "unique non-ancestor merge base for every frozen source pair; whole-record changed keys must be disjoint across tasks.authority_key and development_lines/modules/decisions.id",
473
473
  "bounds": "at most 8 sources per intent, 2 MiB regular document blobs, 64 KiB attributes blobs",
474
474
  "fail_closed": "shared record, metadata/schema/order changes, broad paths, code overlap, duplicate keys, invalid/missing/oversized Git evidence, ancestry dependency, explicit prerequisite or earlier publishing/recovery/attention state",
@@ -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.0",
1074
+ "tool_version": "1.32.2",
1075
1075
  "delivered_closeout_version": 1,
1076
1076
  "retained_archive_version": 1,
1077
1077
  "retention_convergence_version": 2,