worktree-controller 1.31.0__tar.gz → 1.32.1__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.31.0 → worktree_controller-1.32.1}/PKG-INFO +9 -5
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/README.md +8 -4
- worktree_controller-1.32.1/docs/client-integration.md +148 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/docs/operations.md +65 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/docs/reference.md +7 -4
- worktree_controller-1.32.1/examples/client_continuation.py +239 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/pyproject.toml +1 -1
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/pyproject.toml.orig +1 -1
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/__init__.py +1 -1
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/keyed_document_scope.py +16 -10
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/tool_cli_contract.json +2 -2
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/tool_cli_contract.json +2 -2
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/LICENSE +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/docs/architecture.md +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/docs/daily-workflow.md +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/docs/first-change.md +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/docs/getting-started.md +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/docs/publishing.md +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/examples/change_task.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/examples/quickstart.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/activation_layout.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/branch_retirement.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/canonical_refresh.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/canonical_transition.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/cleanup_attempts.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/cleanup_evidence.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/cleanup_recovery.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/cli.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/controller.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/convergence.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/delivered_closeout.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/disposable.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/environment.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/frozen_recovery.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/git_backend.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/governance.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/hardlinks.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/integration_cleanup.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/integration_conflict.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/integration_retry.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/lifecycle.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/lifecycle_history.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/lifecycle_observation.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/metadata_codec.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/metadata_relocation.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/models.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/mount_diagnostics.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/multiple_cleanup_recovery.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/namespace_migration.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/native_session_recovery.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/owner_actions.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/policy.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/preservation.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/preserved_cleanup.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/publication_execution.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/reconciler.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/recovery_guidance.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/registration_backfill.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/registration_prune.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/rendering.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/resource_admission.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/retirement_adapters.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/retirement_execution.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/semantics.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/session.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/session_admission.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/source_cleanup.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/source_preservation.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/source_recovery.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/store.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/task_workflow.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/transaction_scope.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/writer_lease.py +0 -0
- {worktree_controller-1.31.0 → worktree_controller-1.32.1}/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.
|
|
3
|
+
Version: 1.32.1
|
|
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.
|
|
26
|
+
**Current release: 1.32.0 · 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.
|
|
35
|
+
python -m pip install worktree-controller==1.32.0
|
|
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.
|
|
44
|
+
Software version 1.32.0 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.
|
|
53
|
+
git checkout v1.32.0
|
|
54
54
|
python examples/quickstart.py --workspace "$HOME/wc-first-run"
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -76,6 +76,10 @@ for the recorded owner and next formal step. The [everyday task guide](docs/dail
|
|
|
76
76
|
covers simpler start, original-task checkpoint/resume, guarded no-change release and exact
|
|
77
77
|
Finalizer handoff. These CLI 61 entrypoints retain the existing ownership, lease and policy checks.
|
|
78
78
|
|
|
79
|
+
For client developers, the [AI client integration guide](docs/client-integration.md) describes the
|
|
80
|
+
public identity and closeout contract, with an offline example that continues the same unfinished
|
|
81
|
+
task from two independent client processes while preserving its files and checkpoint.
|
|
82
|
+
|
|
79
83
|
## Use WC in a real project
|
|
80
84
|
|
|
81
85
|
A governed repository tracks `.agents/worktree-policy.toml` and registers its checkouts with WC.
|
|
@@ -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.
|
|
9
|
+
**Current release: 1.32.0 · 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.
|
|
18
|
+
python -m pip install worktree-controller==1.32.0
|
|
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.
|
|
27
|
+
Software version 1.32.0 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.
|
|
36
|
+
git checkout v1.32.0
|
|
37
37
|
python examples/quickstart.py --workspace "$HOME/wc-first-run"
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -59,6 +59,10 @@ for the recorded owner and next formal step. The [everyday task guide](docs/dail
|
|
|
59
59
|
covers simpler start, original-task checkpoint/resume, guarded no-change release and exact
|
|
60
60
|
Finalizer handoff. These CLI 61 entrypoints retain the existing ownership, lease and policy checks.
|
|
61
61
|
|
|
62
|
+
For client developers, the [AI client integration guide](docs/client-integration.md) describes the
|
|
63
|
+
public identity and closeout contract, with an offline example that continues the same unfinished
|
|
64
|
+
task from two independent client processes while preserving its files and checkpoint.
|
|
65
|
+
|
|
62
66
|
## Use WC in a real project
|
|
63
67
|
|
|
64
68
|
A governed repository tracks `.agents/worktree-policy.toml` and registers its checkouts with WC.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Integrating an AI client
|
|
2
|
+
|
|
3
|
+
Use the existing public CLI as the boundary between a client and WC. The client implements the
|
|
4
|
+
business task and keeps its durable checkpoint; WC remains the authority for admission, writer
|
|
5
|
+
identity, lifecycle and release. Client names do not grant ownership or write permission.
|
|
6
|
+
|
|
7
|
+
## Check the installed contract
|
|
8
|
+
|
|
9
|
+
Read `worktree-controller --version`, `capabilities --json` and the installed command's `--help`.
|
|
10
|
+
The task-oriented flow below requires CLI 61 and `task_workflow_version=1`. For checkpoint and
|
|
11
|
+
strict closeout, require `task_checkpoint_park_version=1`, `session_closeout_report_version=1`
|
|
12
|
+
and `session_closeout_expectations_version=1`. Storage layout is independently versioned (5 in
|
|
13
|
+
1.31.0). A client must explicitly support the reported contract and required capabilities; a
|
|
14
|
+
newer version number alone is not sufficient. Refuse an unsupported contract without mutation.
|
|
15
|
+
|
|
16
|
+
Invoke commands as argument lists and parse JSON, process exit status and the specific result
|
|
17
|
+
together. Never parse human descriptions, evaluate a returned shell string, open WC's private
|
|
18
|
+
state, recreate a digest or replace a refused operation with raw Git worktree commands.
|
|
19
|
+
|
|
20
|
+
## Keep the task and executor identities separate
|
|
21
|
+
|
|
22
|
+
| Durable business Task keeps | Current executor obtains or observes |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Repository, task key, authority key, recorded owner | Actual live holder PID, boot identity and start time |
|
|
25
|
+
| Exact allocation ID, checkout path and branch | Client, session and run identity |
|
|
26
|
+
| Checkpoint payload, content-addressed ID and bound HEAD | Fresh writer lease and current record version |
|
|
27
|
+
| All allocations acquired by this task/session | Current HEAD, scope, lifecycle and blockers |
|
|
28
|
+
| Finalizer, integration and release evidence when applicable | Current public recovery guidance |
|
|
29
|
+
|
|
30
|
+
Changing AI clients changes the executor identity. It does not create a new business Task,
|
|
31
|
+
transfer its owner, replace its allocation or inherit the previous client's lease. An owner
|
|
32
|
+
transfer uses the existing explicit review/transfer flow. Re-observe identity, HEAD, scope and
|
|
33
|
+
record version before every mutation; a checkpoint is not a reusable authorization credential.
|
|
34
|
+
|
|
35
|
+
## Start, pause and continue
|
|
36
|
+
|
|
37
|
+
1. Inspect `status`, `doctor`, the selected policy and an existing compatible task. Use
|
|
38
|
+
`task start` only for a new task. Admission returns a checkout without writer entitlement.
|
|
39
|
+
2. Bind the actual executor through the supported `writer-session` handshake or
|
|
40
|
+
`writer-lease-acquire`. Pass the exact returned holder/session/run identity to `write-guard`
|
|
41
|
+
for the recorded task, authority, owner, path and operation before writing.
|
|
42
|
+
3. To pause, first persist the existing Task's checkpoint, including its exact HEAD and enough
|
|
43
|
+
context to continue. WC does not copy or verify that payload. Preserve uncommitted files,
|
|
44
|
+
unknown files and generated output. Inspect normal exit with the real full session selector
|
|
45
|
+
while its leases still exist. End through that same selector to record its allocations and
|
|
46
|
+
release only its own leases; inspect any incomplete result. Then use `task finish --outcome
|
|
47
|
+
checkpoint` with its content-addressed ID and HEAD and repeat the strict check. Releasing leases
|
|
48
|
+
individually before session end can lose the live selector's association: expected IDs add
|
|
49
|
+
proof obligations but cannot select a retained checkout or establish that association.
|
|
50
|
+
4. A different client reloads the Task checkpoint, uses public `task show`/`status` to match the
|
|
51
|
+
original allocation, owner, path, HEAD and scope, and refuses drift. Use `task resume` with the
|
|
52
|
+
original owner, then acquire fresh entitlement for the new live executor and repeat the guard.
|
|
53
|
+
A stale recorded lease must be resolved through the original formal recovery; never steal it.
|
|
54
|
+
5. When work is deliverable, use real Finalizer evidence and `task finish --outcome handoff`,
|
|
55
|
+
followed by the existing integration flow. A no-change finish is only for genuine unchanged
|
|
56
|
+
work. A checkpoint pause leaves work unfinished and retains the checkout.
|
|
57
|
+
|
|
58
|
+
See [everyday commands](daily-workflow.md) for exact examples and [operations](operations.md)
|
|
59
|
+
for the existing writer, cleanup, recovery and integration entrypoints.
|
|
60
|
+
|
|
61
|
+
## End every allocation, and report failures accurately
|
|
62
|
+
|
|
63
|
+
Use the real client/session/holder/run selector with `worktree-session-end --check
|
|
64
|
+
--require-complete`. Add every allocation the Task/session knows it acquired with repeatable
|
|
65
|
+
`--expected-allocation-id`; these are additional proof obligations, not mutation selectors.
|
|
66
|
+
Inspect every live, previous and expected row and require `summary.terminal_complete=true`.
|
|
67
|
+
After preparation, end the session with the same selector and expected IDs using
|
|
68
|
+
`--require-complete` without `--check`. Reobserve any refusal through the public recovery guide.
|
|
69
|
+
|
|
70
|
+
An advisory exit 0 is not settlement evidence. A read-only guide may successfully describe a
|
|
71
|
+
blocker. `RESOURCE_WAIT` and an incomplete no-change release return 2; retain the original request
|
|
72
|
+
and any completed prefix. Retry only the pending formal step. Checkout settlement, business
|
|
73
|
+
completion and main publication each require their own evidence. A retained checkpoint can
|
|
74
|
+
settle an executor session while its business Task remains unfinished, but dirty or unclassified
|
|
75
|
+
files still require owner action and keep strict closeout incomplete. Checkpoint success alone
|
|
76
|
+
does not settle that obligation. Preserve the pending result when the original owner continues
|
|
77
|
+
the same Task in a new client; do not recreate an old holder to make it appear complete.
|
|
78
|
+
|
|
79
|
+
## Run the offline continuation example
|
|
80
|
+
|
|
81
|
+
From the source checkout or the 1.32.0 source distribution, with WC 1.31.0 or later installed:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python examples/client_continuation.py --workspace "$HOME/wc-client-example"
|
|
85
|
+
python examples/client_continuation.py --workspace "$HOME/wc-client-example" --resume
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The first invocation requires a new path under an existing parent. Each invocation is a real,
|
|
89
|
+
separate executor process with a different client/session/run identity. The example uses its
|
|
90
|
+
own HOME and a disposable repository, no remote, credentials, companion delivery tools or AI
|
|
91
|
+
service. It verifies original-task continuation, wrong-owner and competing-writer refusals,
|
|
92
|
+
two-checkout closeout, partial-release exit 2 and retry without duplicate completion.
|
|
93
|
+
|
|
94
|
+
The second invocation verifies that the new writer is permitted and the old client identity is
|
|
95
|
+
refused. It preserves the unfinished edit, unknown binary note, original checkpoint and its new
|
|
96
|
+
generation. Only the exact generated fixture created by the example is moved to its preservation
|
|
97
|
+
path; real unknown output still requires its owner's classification/preservation flow.
|
|
98
|
+
`PASS` means the protocol exercise completed. No business task is declared complete or published.
|
|
99
|
+
The original work stays `RETAINED`, with its branch, files and command/closeout evidence available.
|
|
100
|
+
Strict closeout correctly returns 2: the inspection checkout has complete release proof, while
|
|
101
|
+
the dirty unfinished checkout remains pending. Each executor formally ends its own writer session.
|
|
102
|
+
An existing workspace, identity/file drift or repeated resume is refused, preserving evidence.
|
|
103
|
+
|
|
104
|
+
This demonstrates two CLI consumers, not actual invocation or discovery by two AI vendors.
|
|
105
|
+
Vendor-specific discovery and live-client use require their own observed acceptance, as described
|
|
106
|
+
in [checks and deployment evidence](operations.md#checks-and-deployment).
|
|
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
|
+
|
|
135
|
+
## Upgrade an existing managed installation
|
|
136
|
+
|
|
137
|
+
A public package installation in a new environment is separate from activation of a shared
|
|
138
|
+
Controller. Use the site's supported installer, published toolchain compatibility catalog,
|
|
139
|
+
consumer checks and rollout gate. Finish with installed smoke and the default installed doctor.
|
|
140
|
+
Do not repoint the active runtime, modify another client's session or widen accepted versions.
|
|
141
|
+
|
|
142
|
+
Ordinary `controller-activate activate` requires the target to descend from the active commit.
|
|
143
|
+
For an independent public export, 1.32.0 provides the explicit, receipt-bound
|
|
144
|
+
[`release-history-handover/v1` procedure](operations.md#moving-an-exact-exported-release-to-its-public-history).
|
|
145
|
+
It requires exact exported payloads, published release identities and current rollout gates;
|
|
146
|
+
ordinary bootstrap, copying the shared Skill or matching storage versions is not a substitute.
|
|
147
|
+
A catalog refusal blocks deployment while isolated protocol validation and source delivery can
|
|
148
|
+
continue. Record staged, activated and verified results separately.
|
|
@@ -4,6 +4,8 @@ Use this page to select a flow. The [CLI contract](../tool_cli_contract.json) ow
|
|
|
4
4
|
capability and result schemas; installed `--help` owns the arguments available on this host.
|
|
5
5
|
The [technical reference](reference.md) retains detailed protocols and compatibility history;
|
|
6
6
|
the [getting started guide](getting-started.md) provides an isolated first run.
|
|
7
|
+
Client implementers can follow the [AI client integration guide](client-integration.md) and its
|
|
8
|
+
offline cross-client continuation example.
|
|
7
9
|
|
|
8
10
|
## Start or resume development
|
|
9
11
|
|
|
@@ -260,6 +262,69 @@ stage, rollout and activation. CLI, capability and storage versions are independ
|
|
|
260
262
|
Use the existing rollback flow; never repoint the active runtime by hand or activate another
|
|
261
263
|
owner's unintegrated candidate. Finish with installed smoke, rollout/deployment checks and doctor.
|
|
262
264
|
|
|
265
|
+
### Moving an exact exported release to its public history
|
|
266
|
+
|
|
267
|
+
Normal `activate` requires descendant history and normal `rollback` requires ancestor history.
|
|
268
|
+
An independent public export does not satisfy either rule. Since 1.32.0, the existing
|
|
269
|
+
`scripts/controller-activate` entry supports `release-history-handover/v1`; it does not modify
|
|
270
|
+
Controller lifecycle data, Git ancestry, admission policy or writer leases. Run the exact
|
|
271
|
+
published target's maintenance script, with the normal site configuration:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
export CONTROLLER_ROLLOUT_REPO=/absolute/public/canonical-checkout
|
|
275
|
+
export CONTROLLER_ROLLOUT_GIT_SOURCE=https://github.com/your-org/worktree-controller.git
|
|
276
|
+
export CONTROLLER_ROLLOUT_GOVERNED=/absolute/repo-one:/absolute/repo-two
|
|
277
|
+
controller-activate stage --version FULL_TARGET_OID --temp-helper /absolute/tool-temp-dir
|
|
278
|
+
controller-activate history-activate --version FULL_TARGET_OID \
|
|
279
|
+
--request-id original-export-request --handover-baseline FULL_PUBLIC_ROOT_OID \
|
|
280
|
+
--previous-source-repo /absolute/retained-original-source \
|
|
281
|
+
--consumer /absolute/installed-host-capability-command --dry-run
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Repeat `--consumer` for every effective external host and include the site's published
|
|
285
|
+
`--toolchain-compatibility /absolute/catalog.json` when one is configured. The optional checker
|
|
286
|
+
must understand CLI 61 and `controller-runtime-source/v1` (Git Finalizer 1.9.4 or later for the
|
|
287
|
+
bundled checker). Stage writes that immutable, owned source manifest beside the runtime; it binds
|
|
288
|
+
the exact commit, primary source checkout, Git common directory, history root, contract hash and
|
|
289
|
+
Skill hash. Legacy installations without it retain their existing source selection. Invalid or
|
|
290
|
+
changed manifests must fail rather than silently fall back. Keep both source histories and both
|
|
291
|
+
installed runtimes available for verification and rollback.
|
|
292
|
+
|
|
293
|
+
The gate requires a root release whose runtime, packaging, maintenance scripts and complete Skill
|
|
294
|
+
payload exactly equal the original frozen predecessor. Both public baseline and target must have
|
|
295
|
+
live matching stable release tags. The target must descend from that baseline and preserve storage,
|
|
296
|
+
metadata and writer-lease formats and installed Skill bytes. This entry cannot combine namespace,
|
|
297
|
+
policy, Skill or cleanup migrations. Existing resource, metadata and consumer gates still run.
|
|
298
|
+
Live compatible writer sessions may continue; incompatible, stale or incomplete evidence is not
|
|
299
|
+
made eligible by this procedure.
|
|
300
|
+
|
|
301
|
+
Inspect the single JSON result. Dry-run exit 0 means `plan_ready=true`, **not** completed activation.
|
|
302
|
+
Apply the identical request without `--dry-run`, adding its exact `--expected-plan-id`. The plan
|
|
303
|
+
binds the current active pointer, scope and compatibility catalog. The producer saves an immutable
|
|
304
|
+
receipt before switching. Preserve its `receipt` and `receipt_sha256` from the result. On interruption
|
|
305
|
+
or an uncertain result, inspect the original request and active pin before retrying; do not invent
|
|
306
|
+
another request identity. Resume using only the original receipt and SHA256:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
controller-activate history-activate --history-receipt /absolute/original-receipt.json \
|
|
310
|
+
--history-receipt-sha256 ORIGINAL_SHA256 --consumer /absolute/installed-host-capability-command \
|
|
311
|
+
--dry-run
|
|
312
|
+
controller-activate history-rollback --history-receipt /absolute/original-receipt.json \
|
|
313
|
+
--history-receipt-sha256 ORIGINAL_SHA256 --consumer /absolute/installed-host-capability-command \
|
|
314
|
+
--dry-run
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For either action, inspect a fresh plan and apply with its exact plan ID. Keep the original governed
|
|
318
|
+
repositories and consumers; adding current checks is allowed, omitting original checks is refused.
|
|
319
|
+
An explicit rollback request fences subsequent forward reuse of that request. Unknown pending files
|
|
320
|
+
are preserved. An interrupted producer write is preserved byte for byte before an exact retry.
|
|
321
|
+
Exit 0 with `operation_complete=true` proves the requested active pin and postchecks; a repeated
|
|
322
|
+
verified operation may report `activation_performed=false`. Refusal exits 2. Failed activation
|
|
323
|
+
followed by verified predecessor restoration exits 3 with `operation_complete=false`; this is a
|
|
324
|
+
failure, not successful upgrade. If restoration itself cannot pass current gates, the entry reports
|
|
325
|
+
the observed active commit and preserves evidence for the original recovery. Finish with installed
|
|
326
|
+
smoke and the default deployment doctor; runtime activation never grants business release approval.
|
|
327
|
+
|
|
263
328
|
For each external consumer, distinguish three outcomes:
|
|
264
329
|
|
|
265
330
|
| Check | What it proves |
|
|
@@ -34,7 +34,9 @@ for core inspection/allocation; selecting a delivery flow can add explicit evide
|
|
|
34
34
|
Repositories created under the current namespace never invoke it.
|
|
35
35
|
- **Maintainer deployment.** `scripts/controller-activate` and `scripts/controller-runtime` are the
|
|
36
36
|
source-owned runtime activation and start entry points used by a maintainer's host; they are not
|
|
37
|
-
required to install or use the published package.
|
|
37
|
+
required to install or use the published package. Since 1.32.0, their explicit
|
|
38
|
+
`release-history-handover/v1` and `controller-runtime-source/v1` contracts support exact public
|
|
39
|
+
exports and immutable runtime provenance; see the [maintenance procedure](operations.md#moving-an-exact-exported-release-to-its-public-history).
|
|
38
40
|
|
|
39
41
|
From 1.30.1, a completed cleanup at a retained source may survive normal park/resume
|
|
40
42
|
cycles followed by a formal handoff of the same HEAD and owner. Verification requires
|
|
@@ -1476,9 +1478,10 @@ checkout rejects feature writes. `READY_TO_PUBLISH` is write-fenced until explic
|
|
|
1476
1478
|
out-of-band HEAD, content, record, or binding drift makes old evidence unusable.
|
|
1477
1479
|
|
|
1478
1480
|
The bounded `integration-keyed-document-scope/v1` refinement applies only when the entire
|
|
1479
|
-
overlap is
|
|
1480
|
-
|
|
1481
|
-
|
|
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`
|
|
1482
1485
|
or `development_lines/modules/decisions.id`, including additions and deletions. All changed key
|
|
1483
1486
|
sets must be disjoint. Metadata, schema, ordering, duplicate keys, missing/oversized/non-regular
|
|
1484
1487
|
blobs, unknown evidence, broad paths and code overlaps keep the ordinary file fence. Explicit
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Exercise two independent client processes continuing one unfinished task, offline."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from quickstart import POLICY
|
|
14
|
+
|
|
15
|
+
OWNER = "demo-task-owner"
|
|
16
|
+
TASK = "continued-work"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def require(condition, message):
|
|
20
|
+
if not condition:
|
|
21
|
+
raise RuntimeError(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
26
|
+
parser.add_argument("--workspace", type=Path, required=True,
|
|
27
|
+
help="new directory under an existing parent on the first invocation")
|
|
28
|
+
parser.add_argument("--resume", action="store_true", help="continue the example's original task")
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
workspace = args.workspace.absolute()
|
|
31
|
+
if workspace.is_symlink():
|
|
32
|
+
parser.error("workspace must not be a symlink")
|
|
33
|
+
if not args.resume and (workspace.exists() or not workspace.parent.is_dir()):
|
|
34
|
+
parser.error("workspace must be a new path under an existing directory")
|
|
35
|
+
if sys.platform != "linux" or sys.version_info[:2] != (3, 12):
|
|
36
|
+
parser.error("the example requires Linux and Python 3.12")
|
|
37
|
+
controller = Path(sys.executable).with_name("worktree-controller")
|
|
38
|
+
if not controller.is_file():
|
|
39
|
+
found = shutil.which("worktree-controller")
|
|
40
|
+
if not found:
|
|
41
|
+
parser.error("install WC 1.31.0 or later in this Python environment first")
|
|
42
|
+
controller = Path(found)
|
|
43
|
+
session_end = controller.with_name("worktree-session-end")
|
|
44
|
+
if not session_end.is_file() or not shutil.which("git"):
|
|
45
|
+
parser.error("Git and worktree-session-end must be installed")
|
|
46
|
+
|
|
47
|
+
# Never read a caller's checkpoint or append logs through a symlink.
|
|
48
|
+
if args.resume:
|
|
49
|
+
for name in ("task-checkpoint.json", "commands.jsonl", "home", "main", "continued-work"):
|
|
50
|
+
path = workspace / name
|
|
51
|
+
require(path.exists() and not path.is_symlink(), f"missing or symlink example path: {name}")
|
|
52
|
+
checkpoint_bytes = (workspace / "task-checkpoint.json").read_bytes()
|
|
53
|
+
saved = json.loads(checkpoint_bytes)
|
|
54
|
+
require(saved["example"] == "wc-client-continuation/v1", "unknown example checkpoint")
|
|
55
|
+
else:
|
|
56
|
+
workspace.mkdir()
|
|
57
|
+
(workspace / "home").mkdir()
|
|
58
|
+
(workspace / "tmp").mkdir()
|
|
59
|
+
|
|
60
|
+
home, repo = workspace / "home", workspace / "main"
|
|
61
|
+
checkout, audit = workspace / TASK, workspace / "inspection"
|
|
62
|
+
environment = {key: os.environ[key] for key in ("PATH", "LANG", "LC_ALL", "TZ") if key in os.environ}
|
|
63
|
+
environment.update(HOME=str(home), TMPDIR=str(workspace / "tmp"),
|
|
64
|
+
XDG_STATE_HOME=str(home / ".local/state"),
|
|
65
|
+
XDG_CONFIG_HOME=str(home / ".config"), XDG_CACHE_HOME=str(home / ".cache"),
|
|
66
|
+
GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, GIT_TERMINAL_PROMPT="0",
|
|
67
|
+
PYTHONNOUSERSITE="1", PYTHONDONTWRITEBYTECODE="1")
|
|
68
|
+
client = "example-client-b" if args.resume else "example-client-a"
|
|
69
|
+
identity = ["--client-id", client, "--session-id", client + "-session", "--run-id", client + "-run"]
|
|
70
|
+
|
|
71
|
+
def run(*arguments, expected=0):
|
|
72
|
+
command = [str(value) for value in arguments]
|
|
73
|
+
result = subprocess.run(command, cwd=workspace, env=environment, capture_output=True,
|
|
74
|
+
text=True, timeout=120, check=False)
|
|
75
|
+
with (workspace / "commands.jsonl").open("a") as stream:
|
|
76
|
+
stream.write(json.dumps({"argv": command, "returncode": result.returncode,
|
|
77
|
+
"stdout": result.stdout, "stderr": result.stderr}) + "\n")
|
|
78
|
+
require(result.returncode == expected,
|
|
79
|
+
f"{command[0]} exited {result.returncode}, expected {expected}; see commands.jsonl")
|
|
80
|
+
return result.stdout
|
|
81
|
+
|
|
82
|
+
def wc(command, *arguments, expected=0):
|
|
83
|
+
if command == "task":
|
|
84
|
+
action, *arguments = arguments
|
|
85
|
+
command_args = [command, action, "--repo", repo, "--json", *arguments]
|
|
86
|
+
else:
|
|
87
|
+
command_args = [command, "--repo", repo, "--json", *arguments]
|
|
88
|
+
payload = json.loads(run(controller, *command_args, expected=expected))
|
|
89
|
+
if expected == 0:
|
|
90
|
+
require(not payload.get("errors"), f"Controller refused {command}")
|
|
91
|
+
return payload
|
|
92
|
+
|
|
93
|
+
def row(path):
|
|
94
|
+
return next(item for item in wc("status")["worktrees"] if item["path"] == str(path))
|
|
95
|
+
|
|
96
|
+
def guard(path, task=TASK, *, selected_identity=None, owner=OWNER, expected=0):
|
|
97
|
+
return wc("write-guard", "--path", path, "--task-key", task, "--authority-key", task,
|
|
98
|
+
"--owner", owner, "--holder", os.getpid(), "--operation", "feature-write",
|
|
99
|
+
*(identity if selected_identity is None else selected_identity), expected=expected)
|
|
100
|
+
|
|
101
|
+
def acquire(path):
|
|
102
|
+
return wc("writer-lease-acquire", "--path", path, "--owner", OWNER,
|
|
103
|
+
"--expected-record-version", row(path)["record_version"],
|
|
104
|
+
"--holder-pid", os.getpid(), *identity)["decision"]
|
|
105
|
+
|
|
106
|
+
def closeout(allocations, name, *, expected=2, checking=True):
|
|
107
|
+
options = [option for allocation in allocations
|
|
108
|
+
for option in ("--expected-allocation-id", allocation)]
|
|
109
|
+
result = json.loads(run(session_end, "--path", repo, *identity, "--holder", os.getpid(),
|
|
110
|
+
*options, *(["--check"] if checking else []),
|
|
111
|
+
"--require-complete", expected=expected))
|
|
112
|
+
(workspace / name).write_text(json.dumps(result, indent=2) + "\n")
|
|
113
|
+
require(result["summary"]["terminal_complete"] is (expected == 0), "incorrect closeout summary")
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
capabilities = wc("capabilities")["decision"]
|
|
117
|
+
require(capabilities.get("contract_version") == 61
|
|
118
|
+
and capabilities.get("storage_layout_version") == 5,
|
|
119
|
+
"unsupported CLI or storage contract; no repository operation was started")
|
|
120
|
+
for capability in ("task_workflow_version", "session_closeout_expectations_version",
|
|
121
|
+
"session_closeout_report_version", "task_checkpoint_park_version"):
|
|
122
|
+
require(capabilities.get(capability) == 1, f"unsupported capability: {capability}")
|
|
123
|
+
|
|
124
|
+
if not args.resume:
|
|
125
|
+
run("git", "init", "--initial-branch=main", repo)
|
|
126
|
+
policy = repo / ".agents/worktree-policy.toml"
|
|
127
|
+
policy.parent.mkdir()
|
|
128
|
+
policy.write_text(POLICY)
|
|
129
|
+
(repo / "README.md").write_text("# Offline client continuation\n")
|
|
130
|
+
(repo / ".gitignore").write_text("generated/\n")
|
|
131
|
+
run("git", "-C", repo, "add", "README.md", ".gitignore", ".agents/worktree-policy.toml")
|
|
132
|
+
run("git", "-C", repo, "-c", "user.name=WC Example", "-c", "user.email=demo@example.invalid",
|
|
133
|
+
"-c", "commit.gpgsign=false", "commit", "-m", "Initialize disposable fixture")
|
|
134
|
+
wc("adopt", "--path", repo, "--task-key", "control", "--authority-key", "control",
|
|
135
|
+
"--module", "example", "--role", "control-plane", "--owner", OWNER, "--lifecycle", "ACTIVE")
|
|
136
|
+
for path, task in ((checkout, TASK), (audit, "inspection")):
|
|
137
|
+
wc("task", "start", "--task-key", task, "--owner", OWNER, "--scope", "README.md",
|
|
138
|
+
"--base-ref", "main", "--path", path)
|
|
139
|
+
original = row(checkout)
|
|
140
|
+
allocations = [original["allocation_id"], row(audit)["allocation_id"]]
|
|
141
|
+
leases = [acquire(checkout), acquire(audit)]
|
|
142
|
+
require(guard(checkout)["decision"]["code"] == "WRITE_ALLOWED", "writer refused")
|
|
143
|
+
wrong = guard(checkout, owner="another-owner", expected=1)
|
|
144
|
+
require(wrong["decision"]["code"] == "BLOCK_OWNER_MISMATCH", "owner negative control failed")
|
|
145
|
+
refused = wc("writer-lease-acquire", "--path", checkout, "--owner", OWNER,
|
|
146
|
+
"--client-id", "competing-client", "--session-id", "competing-session",
|
|
147
|
+
"--holder-pid", os.getpid(), expected=2)
|
|
148
|
+
require(refused["errors"][0]["code"] == "WRITER_LEASE_HELD", "competing writer was not refused")
|
|
149
|
+
(checkout / "README.md").write_text("# Offline client continuation\nClient A unfinished edit.\n")
|
|
150
|
+
(checkout / "owner-note.bin").write_bytes(b"preserve unknown task evidence\x00\xff")
|
|
151
|
+
require(guard(audit, "inspection")["decision"]["code"] == "WRITE_ALLOWED", "audit writer refused")
|
|
152
|
+
generated = audit / "generated"
|
|
153
|
+
generated.mkdir()
|
|
154
|
+
(generated / "example-owned.bin").write_bytes(b"example-owned generated fixture\x00")
|
|
155
|
+
saved = {"example": "wc-client-continuation/v1", "task": {key: original[key] for key in
|
|
156
|
+
("allocation_id", "task_key", "authority_key", "owner", "path", "branch", "head")},
|
|
157
|
+
"allocations": allocations, "first_holder": leases[0]["holder"],
|
|
158
|
+
"files": {name: (checkout / name).read_bytes().hex() for name in ("README.md", "owner-note.bin")}}
|
|
159
|
+
checkpoint_bytes = (json.dumps(saved, sort_keys=True) + "\n").encode()
|
|
160
|
+
(workspace / "task-checkpoint.json").write_bytes(checkpoint_bytes)
|
|
161
|
+
closeout(allocations, "a-before-end.json")
|
|
162
|
+
# End through the real selector while it can still observe both leases.
|
|
163
|
+
# This releases only this session's writers and retains pending owner work.
|
|
164
|
+
closeout(allocations, "a-ended.json", checking=False)
|
|
165
|
+
wc("task", "finish", "--task-key", TASK, "--owner", OWNER, "--outcome", "checkpoint",
|
|
166
|
+
"--checkpoint-id", "sha256:" + hashlib.sha256(checkpoint_bytes).hexdigest(),
|
|
167
|
+
"--checkpoint-head", original["head"], "--waiting-on", "Continue with another client")
|
|
168
|
+
partial = wc("task", "finish", "--task-key", "inspection", "--owner", OWNER,
|
|
169
|
+
"--outcome", "no-change", expected=2)["decision"]
|
|
170
|
+
require(partial["code"] == "TASK_RELEASE_BLOCKED" and not partial["operation_complete"],
|
|
171
|
+
"partial release was reported as success")
|
|
172
|
+
closeout(allocations, "a-incomplete.json", expected=2)
|
|
173
|
+
# Only this example's exact fixture is moved. Real unknown output needs its owner flow.
|
|
174
|
+
(generated / "example-owned.bin").rename(workspace / "preserved-generated.bin")
|
|
175
|
+
generated.rmdir()
|
|
176
|
+
retried = wc("task", "finish", "--task-key", "inspection", "--owner", OWNER,
|
|
177
|
+
"--outcome", "no-change")["decision"]
|
|
178
|
+
require([step["code"] for step in retried["operations"]] == ["RELEASED"]
|
|
179
|
+
and retried["checkout_released"], "completion was replayed")
|
|
180
|
+
final = closeout(allocations, "a-closeout.json")
|
|
181
|
+
require(final["summary"]["complete"] == 1 and final["summary"]["pending"] == 1,
|
|
182
|
+
"expected one released checkout and one unfinished checkout")
|
|
183
|
+
print(f"PAUSED: checkpoint saved; inspection released; unfinished task still needs its owner: {workspace}")
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
current = row(checkout)
|
|
187
|
+
require(all(current[key] == value for key, value in saved["task"].items()), "original task identity changed")
|
|
188
|
+
require(current["lifecycle"] == "RETAINED", "original task is not retained")
|
|
189
|
+
require(current["wait_checkpoint"]["artifact_id"] == "sha256:" + hashlib.sha256(checkpoint_bytes).hexdigest(),
|
|
190
|
+
"original checkpoint changed")
|
|
191
|
+
for name, value in saved["files"].items():
|
|
192
|
+
require(not (checkout / name).is_symlink() and (checkout / name).is_file(),
|
|
193
|
+
f"unfinished file is not a regular non-symlink file: {name}")
|
|
194
|
+
require((checkout / name).read_bytes().hex() == value, f"unfinished file changed: {name}")
|
|
195
|
+
refused = wc("task", "resume", "--task-key", TASK, "--owner", "another-owner", expected=2)
|
|
196
|
+
require(refused["errors"][0]["code"] == "OWNER_MISMATCH", "owner change was accepted")
|
|
197
|
+
wc("task", "resume", "--task-key", TASK, "--owner", OWNER)
|
|
198
|
+
lease = acquire(checkout)
|
|
199
|
+
require(any(lease["holder"][key] != saved["first_holder"][key] for key in ("pid", "boot_id", "start_ticks")),
|
|
200
|
+
"use a separate process to resume")
|
|
201
|
+
old = ["--client-id", "example-client-a", "--session-id", "example-client-a-session",
|
|
202
|
+
"--run-id", "example-client-a-run"]
|
|
203
|
+
require(guard(checkout, selected_identity=old, expected=1)["decision"]["code"] == "BLOCK_WRITER_LEASE_MISMATCH",
|
|
204
|
+
"old client identity still authorizes writes")
|
|
205
|
+
require(guard(checkout)["decision"]["code"] == "WRITE_ALLOWED", "new writer refused")
|
|
206
|
+
with (checkout / "README.md").open("a") as stream:
|
|
207
|
+
stream.write("Client B continued the same task.\n")
|
|
208
|
+
next_checkpoint = {**saved, "previous_checkpoint_sha256": hashlib.sha256(checkpoint_bytes).hexdigest(),
|
|
209
|
+
"second_holder": lease["holder"], "files": {name: (checkout / name).read_bytes().hex()
|
|
210
|
+
for name in saved["files"]}}
|
|
211
|
+
next_bytes = (json.dumps(next_checkpoint, sort_keys=True) + "\n").encode()
|
|
212
|
+
require(not (workspace / "continued-checkpoint.json").exists(), "continued checkpoint already exists")
|
|
213
|
+
(workspace / "continued-checkpoint.json").write_bytes(next_bytes)
|
|
214
|
+
closeout(saved["allocations"], "b-ended.json", checking=False)
|
|
215
|
+
wc("task", "finish", "--task-key", TASK, "--owner", OWNER, "--outcome", "checkpoint",
|
|
216
|
+
"--checkpoint-id", "sha256:" + hashlib.sha256(next_bytes).hexdigest(), "--checkpoint-head", current["head"],
|
|
217
|
+
"--waiting-on", "Example finished; business work remains unfinished")
|
|
218
|
+
final_closeout = closeout(saved["allocations"], "b-closeout.json")
|
|
219
|
+
require(final_closeout["summary"]["complete"] == 1 and final_closeout["summary"]["pending"] == 1,
|
|
220
|
+
"unfinished files must remain pending")
|
|
221
|
+
final = row(checkout)
|
|
222
|
+
require(final["allocation_id"] == saved["task"]["allocation_id"] and final["lifecycle"] == "RETAINED",
|
|
223
|
+
"continuation replaced the original task")
|
|
224
|
+
require((checkout / "owner-note.bin").read_bytes().hex() == saved["files"]["owner-note.bin"], "unknown file lost")
|
|
225
|
+
require(wc("doctor")["decision"]["code"] == "PASS", "doctor failed")
|
|
226
|
+
result = {"protocol_acceptance_complete": True, "business_task_completed": False,
|
|
227
|
+
"published": False, "checkout_retained": True, "session_closeout_complete": False,
|
|
228
|
+
"allocation_id": final["allocation_id"],
|
|
229
|
+
"first_holder": saved["first_holder"], "second_holder": lease["holder"]}
|
|
230
|
+
(workspace / "result.json").write_text(json.dumps(result, indent=2) + "\n")
|
|
231
|
+
print(f"PASS: two client processes continued one task; closeout remains pending; files and logs retained: {workspace}")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
try:
|
|
236
|
+
main()
|
|
237
|
+
except (OSError, RuntimeError, ValueError, KeyError, StopIteration, subprocess.SubprocessError) as error:
|
|
238
|
+
print(f"Example stopped; existing results are preserved. {error}", file=sys.stderr)
|
|
239
|
+
raise SystemExit(1) from error
|
|
@@ -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)
|
|
@@ -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.
|
|
1074
|
+
"tool_version": "1.32.1",
|
|
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.
|
|
1074
|
+
"tool_version": "1.32.1",
|
|
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.31.0 → worktree_controller-1.32.1}/src/worktree_controller/controller.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/convergence.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/disposable.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/environment.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/frozen_recovery.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/git_backend.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/governance.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/hardlinks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/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.31.0 → worktree_controller-1.32.1}/src/worktree_controller/owner_actions.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/preservation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/reconciler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/rendering.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/semantics.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/session.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/source_cleanup.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/source_recovery.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/task_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/writer_lease.py
RENAMED
|
File without changes
|
{worktree_controller-1.31.0 → worktree_controller-1.32.1}/src/worktree_controller/writer_session.py
RENAMED
|
File without changes
|