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.
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/PKG-INFO +5 -5
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/README.md +4 -4
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/architecture.md +2 -2
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/client-integration.md +27 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/reference.md +4 -3
- worktree_controller-1.32.2/docs/scope.md +86 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/pyproject.toml +1 -1
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/pyproject.toml.orig +1 -1
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/__init__.py +1 -1
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/canonical_transition.py +2 -2
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/controller.py +4 -20
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/convergence.py +2 -2
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/delivered_closeout.py +6 -6
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/frozen_recovery.py +3 -3
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/integration_conflict.py +4 -3
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/integration_retry.py +10 -8
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/keyed_document_scope.py +16 -10
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle.py +4 -64
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/reconciler.py +3 -2
- worktree_controller-1.32.2/src/worktree_controller/record_updates.py +54 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/registration_prune.py +2 -2
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/semantics.py +24 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/tool_cli_contract.json +2 -2
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/tool_cli_contract.json +2 -2
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/LICENSE +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/daily-workflow.md +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/first-change.md +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/getting-started.md +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/operations.md +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/docs/publishing.md +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/examples/change_task.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/examples/client_continuation.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/examples/quickstart.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/activation_layout.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/branch_retirement.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/canonical_refresh.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_attempts.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_evidence.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cleanup_recovery.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/cli.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/disposable.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/environment.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/git_backend.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/governance.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/hardlinks.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/integration_cleanup.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle_history.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle_observation.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/metadata_codec.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/metadata_relocation.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/models.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/mount_diagnostics.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/multiple_cleanup_recovery.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/namespace_migration.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/native_session_recovery.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/owner_actions.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/policy.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/preservation.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/preserved_cleanup.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/publication_execution.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/recovery_guidance.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/registration_backfill.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/rendering.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/resource_admission.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/retirement_adapters.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/retirement_execution.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/session.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/session_admission.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_cleanup.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_preservation.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_recovery.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/store.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/task_workflow.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/transaction_scope.py +0 -0
- {worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/writer_lease.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1482
|
-
|
|
1483
|
-
|
|
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.
|
|
@@ -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 =
|
|
277
|
+
updated = updated_record(
|
|
278
278
|
record,
|
|
279
279
|
"CANONICAL_MAIN_REFRESHED" if refresh_main else "CANONICAL_TRANSITIONED",
|
|
280
280
|
lifecycle="ACTIVE",
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/controller.py
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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):
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/convergence.py
RENAMED
|
@@ -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 .
|
|
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
|
|
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 :=
|
|
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 .
|
|
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 =
|
|
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 .
|
|
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 =
|
|
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,
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/frozen_recovery.py
RENAMED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
88
|
-
resolved =
|
|
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
|
|
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
|
|
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 .
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
456
|
+
updated = updated_intent(intent, "RETRY_PREREQUISITE_BOUND")
|
|
455
457
|
else:
|
|
456
|
-
updated =
|
|
458
|
+
updated = updated_intent(
|
|
457
459
|
intent,
|
|
458
460
|
"PREPARATION_RETRY_QUEUED",
|
|
459
461
|
state="QUEUED",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Prove independent
|
|
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/
|
|
17
|
-
|
|
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
|
|
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,
|
|
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 !=
|
|
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)
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/lifecycle.py
RENAMED
|
@@ -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,
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/reconciler.py
RENAMED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
346
|
+
updated = updated_record(
|
|
347
347
|
record, "REGISTRATION_PRUNED", lifecycle="RELEASED", owner="unassigned"
|
|
348
348
|
)
|
|
349
349
|
store.replace_record(updated)
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/semantics.py
RENAMED
|
@@ -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/
|
|
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.
|
|
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/
|
|
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.
|
|
1074
|
+
"tool_version": "1.32.2",
|
|
1075
1075
|
"delivered_closeout_version": 1,
|
|
1076
1076
|
"retained_archive_version": 1,
|
|
1077
1077
|
"retention_convergence_version": 2,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/disposable.py
RENAMED
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/environment.py
RENAMED
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/git_backend.py
RENAMED
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/governance.py
RENAMED
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/hardlinks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/metadata_codec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/owner_actions.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/preservation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/rendering.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/session.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_cleanup.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/source_recovery.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/task_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/writer_lease.py
RENAMED
|
File without changes
|
{worktree_controller-1.32.0 → worktree_controller-1.32.2}/src/worktree_controller/writer_session.py
RENAMED
|
File without changes
|