worktree-runtime 0.1.0a3__tar.gz → 0.1.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.
Files changed (100) hide show
  1. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/Cargo.lock +3 -3
  2. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/Cargo.toml +3 -3
  3. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/LICENSE +7 -1
  4. worktree_runtime-0.1.1/PKG-INFO +127 -0
  5. worktree_runtime-0.1.1/README.md +102 -0
  6. worktree_runtime-0.1.1/crates/worktree_runtime_core/AGENTS.md +55 -0
  7. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/application/binding_lifecycle.rs +337 -0
  8. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/application/git_worktree_service.rs +126 -0
  9. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/application/validation.rs +39 -9
  10. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/application.rs +4 -0
  11. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/infrastructure/audit_record.rs → worktree_runtime-0.1.1/crates/worktree_runtime_core/src/domain/audit.rs +40 -57
  12. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/binding.rs +11 -2
  13. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/paths.rs +22 -15
  14. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/domain/porcelain.rs +132 -0
  15. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/domain.rs +7 -0
  16. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/audit_dir.rs +39 -0
  17. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/audit_file_io.rs +82 -6
  18. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/audit_reader.rs +82 -0
  19. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/audit_writer.rs +145 -0
  20. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/binding_store.rs +305 -0
  21. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/git_adapter.rs +181 -0
  22. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/github_policy_loader.rs +88 -0
  23. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/project_id_resolver.rs +177 -0
  24. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/worktree_base_resolver.rs +111 -125
  25. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/worktree_config_loader.rs +43 -13
  26. worktree_runtime-0.1.1/crates/worktree_runtime_core/src/infrastructure/worktree_list_reader.rs +75 -0
  27. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure.rs +3 -2
  28. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/tests/common/mod.rs +103 -5
  29. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/e2e}/binding_lifecycle_tests.rs +123 -41
  30. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/e2e/git_worktree_service_tests.rs +215 -0
  31. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/e2e/main.rs +8 -0
  32. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/e2e}/validation_tests.rs +51 -7
  33. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/audit_dir_tests.rs +84 -0
  34. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/audit_file_io_tests.rs +169 -0
  35. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/audit_reader_tests.rs +108 -0
  36. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration}/audit_writer_tests.rs +195 -216
  37. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/binding_store_tests.rs +354 -0
  38. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/git_adapter_tests.rs +73 -0
  39. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/github_policy_loader_tests.rs +177 -0
  40. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/main.rs +16 -0
  41. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration/project_id_resolver_tests.rs +322 -0
  42. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration}/worktree_base_resolver_tests.rs +85 -43
  43. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration}/worktree_config_loader_tests.rs +57 -17
  44. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/integration}/worktree_list_reader_tests.rs +42 -86
  45. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit/audit_tests.rs +147 -0
  46. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit}/binding_tests.rs +9 -3
  47. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit}/env_keys_tests.rs +4 -3
  48. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit}/git_worktree_tests.rs +7 -2
  49. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit/github_policy_tests.rs +31 -0
  50. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit/main.rs +9 -0
  51. {worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests → worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit}/paths_tests.rs +33 -3
  52. worktree_runtime-0.1.1/crates/worktree_runtime_core/tests/unit/porcelain_tests.rs +82 -0
  53. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/Cargo.toml +6 -0
  54. worktree_runtime-0.1.1/crates/worktree_runtime_pyo3/src/convert.rs +163 -0
  55. worktree_runtime-0.1.1/crates/worktree_runtime_pyo3/src/functions.rs +491 -0
  56. worktree_runtime-0.1.1/crates/worktree_runtime_pyo3/src/lib.rs +134 -0
  57. worktree_runtime-0.1.1/crates/worktree_runtime_pyo3/src/types.rs +575 -0
  58. worktree_runtime-0.1.1/crates/worktree_runtime_pyo3/tests/pyo3_signature_freeze_tests.rs +173 -0
  59. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/pyproject.toml +20 -3
  60. worktree_runtime-0.1.1/src/worktree_runtime/AGENTS.md +21 -0
  61. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/src/worktree_runtime/SURFACE.yaml +23 -0
  62. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/src/worktree_runtime/api/__init__.py +16 -35
  63. worktree_runtime-0.1.1/src/worktree_runtime/api/__init__.pyi +356 -0
  64. worktree_runtime-0.1.1/src/worktree_runtime/presentation/audit_cli.py +91 -0
  65. worktree_runtime-0.1.0a3/PKG-INFO +0 -9
  66. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/AGENTS.md +0 -9
  67. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/application/audit_service.rs +0 -7
  68. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/application/binding_lifecycle.rs +0 -250
  69. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/application/checkout_service.rs +0 -97
  70. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/application.rs +0 -5
  71. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/domain/porcelain.rs +0 -35
  72. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/domain.rs +0 -6
  73. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/infrastructure/audit_writer.rs +0 -184
  74. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/infrastructure/binding_store.rs +0 -139
  75. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/infrastructure/git_adapter.rs +0 -57
  76. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/infrastructure/path_resolution.rs +0 -8
  77. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/infrastructure/project_id_resolver.rs +0 -190
  78. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/src/infrastructure/worktree_list_reader.rs +0 -178
  79. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests/audit_file_io_tests.rs +0 -103
  80. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests/audit_record_tests.rs +0 -45
  81. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests/audit_service_tests.rs +0 -44
  82. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests/binding_store_tests.rs +0 -215
  83. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests/checkout_service_tests.rs +0 -124
  84. worktree_runtime-0.1.0a3/crates/worktree_runtime_core/tests/project_id_resolver_tests.rs +0 -407
  85. worktree_runtime-0.1.0a3/crates/worktree_runtime_pyo3/src/lib.rs +0 -1075
  86. worktree_runtime-0.1.0a3/crates/worktree_runtime_pyo3/tests/pyo3_signature_freeze_tests.rs +0 -31
  87. worktree_runtime-0.1.0a3/src/worktree_runtime/AGENTS.md +0 -14
  88. worktree_runtime-0.1.0a3/src/worktree_runtime/api/__init__.pyi +0 -144
  89. worktree_runtime-0.1.0a3/src/worktree_runtime/api.pyi +0 -144
  90. worktree_runtime-0.1.0a3/src/worktree_runtime/presentation/audit_cli.py +0 -68
  91. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/Cargo.toml +0 -0
  92. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/git_worktree.rs +0 -0
  93. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/domain/github_policy.rs +0 -0
  94. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/infrastructure/env_keys.rs +0 -0
  95. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_core/src/lib.rs +0 -0
  96. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/crates/worktree_runtime_pyo3/AGENTS.md +0 -0
  97. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/src/_worktree_runtime_rust/__init__.py +0 -0
  98. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/src/worktree_runtime/__init__.py +0 -0
  99. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/src/worktree_runtime/presentation/__init__.py +0 -0
  100. {worktree_runtime-0.1.0a3 → worktree_runtime-0.1.1}/src/worktree_runtime/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  # This file is automatically @generated by Cargo.
2
2
  # It is not intended for manual editing.
3
- version = 3
3
+ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "android_system_properties"
@@ -743,7 +743,7 @@ dependencies = [
743
743
 
744
744
  [[package]]
745
745
  name = "worktree_runtime_core"
746
- version = "0.1.0-alpha.3"
746
+ version = "0.1.1"
747
747
  dependencies = [
748
748
  "chrono",
749
749
  "fs2",
@@ -760,7 +760,7 @@ dependencies = [
760
760
 
761
761
  [[package]]
762
762
  name = "worktree_runtime_pyo3"
763
- version = "0.1.0-alpha.3"
763
+ version = "0.1.1"
764
764
  dependencies = [
765
765
  "chrono",
766
766
  "pyo3",
@@ -3,9 +3,9 @@ resolver = "2"
3
3
  members = ["crates/worktree_runtime_core", "crates/worktree_runtime_pyo3"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.1.0-alpha.3"
6
+ version = "0.1.1"
7
7
  edition = "2021"
8
- rust-version = "1.75"
8
+ rust-version = "1.85"
9
9
  license = "LicenseRef-Proprietary"
10
10
  publish = false
11
11
 
@@ -18,7 +18,7 @@ sha1 = "0.10"
18
18
  fs2 = "0.4"
19
19
  tempfile = "3.10"
20
20
  wait-timeout = "0.2"
21
- pyo3 = { version = "0.29", features = ["extension-module"] }
21
+ pyo3 = { version = "0.29", features = ["abi3-py310"] }
22
22
  chrono = "0.4"
23
23
 
24
24
  [profile.release]
@@ -1,4 +1,4 @@
1
- Copyright (c) 2026 adaptive-task contributors
1
+ Copyright (c) 2026 bamboocity
2
2
  All rights reserved.
3
3
 
4
4
  This software and associated documentation files (the "Software") are
@@ -6,6 +6,12 @@ proprietary. Unauthorized copying, modification, distribution, or use of
6
6
  the Software, via any medium, is strictly prohibited except as expressly
7
7
  authorized in writing by the copyright holder.
8
8
 
9
+ The Software is distributed on the Python Package Index as binary wheels
10
+ and as a source distribution (sdist) that contains this source code. That
11
+ distribution is provided solely so the package can be installed and built;
12
+ it does not grant any right to copy, modify, or redistribute the source
13
+ beyond what is necessary to install the Software.
14
+
9
15
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
10
16
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
11
17
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: worktree-runtime
3
+ Version: 0.1.1
4
+ Classifier: Development Status :: 5 - Production/Stable
5
+ Classifier: License :: Other/Proprietary License
6
+ Classifier: Operating System :: MacOS
7
+ Classifier: Operating System :: Microsoft :: Windows
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Rust
12
+ Classifier: Typing :: Typed
13
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
14
+ Requires-Dist: pyyaml>=6.0 ; extra == 'dev'
15
+ Requires-Dist: mypy>=1.14 ; extra == 'dev'
16
+ Provides-Extra: dev
17
+ License-File: LICENSE
18
+ Summary: Worktree identity, binding persistence, and checkout substrate
19
+ Author: bamboocity
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
22
+ Project-URL: Changelog, https://github.com/bamboocity/worktree-runtime/blob/main/CHANGELOG.md
23
+ Project-URL: Repository, https://github.com/bamboocity/worktree-runtime
24
+
25
+ # worktree-runtime
26
+
27
+ Library for worktree **identity**, **binding persistence**, **checkout
28
+ validation**, and **path resolution**.
29
+
30
+ Consumers (adaptive-task / Noveler) call this so they do not each invent
31
+ project id, session↔worktree binding, main-vs-linked checks, or
32
+ state / shared paths. The public symbol list is
33
+ [`src/worktree_runtime/SURFACE.yaml`](src/worktree_runtime/SURFACE.yaml).
34
+
35
+ Binding store API: `register_binding`, `load_binding`, `recover_binding`,
36
+ `list_bindings`, and `remove_binding`. `git worktree remove` and vanish stay
37
+ in the consumer.
38
+
39
+ Rust is the implementation SSOT. Python is a shim, typing stubs, contract tests,
40
+ and a thin audit CLI. Missing native support is an install error; there is no
41
+ silent Python fallback in this project.
42
+
43
+ ## Scope
44
+
45
+ This crate answers four questions and records consumer mutations:
46
+
47
+ | Responsibility | What the library does |
48
+ | --- | --- |
49
+ | Identity | Resolve project id, main branches, GitHub policy, and the `NOVELER_*` / location env catalog from cwd, env, git, and project config. |
50
+ | Binding | Persist, load, recover, and validate a session↔worktree record (`WorktreeBinding`). Reject before treating a cwd as bound. |
51
+ | Checkout | Resolve a `GitWorktree` (`rev-parse`), classify main vs linked, list porcelain entries, emit reason codes. |
52
+ | Path | Resolve worktree base / dir / state dir / known bases with fixed precedence. `NOVELER_WORKTREE_BASE` and `config/worktree.yaml` overrides apply only when `CI` / `GITHUB_ACTIONS` is set; elsewhere the base is `~/.adaptive-task/worktrees/<project_id>`. Normalize and fingerprint paths. |
53
+
54
+ Audit (`record_event` / `read_records` / rotation, plus
55
+ `presentation/audit_cli.py`) is append-only logging of consumer git
56
+ mutations (`worktree_remove`, `branch_delete`). It is not a session log.
57
+
58
+ ### Out of scope
59
+
60
+ - **Agent isolation.** Codex Desktop, Cursor, and Claude Code create a
61
+ worktree (or checkout) so one chat does not dirty the user's main tree.
62
+ That sandbox is a different layer. This package does not replace it.
63
+ - **Session orchestration.** `adaptive-task init` / `next` / `report` stay
64
+ in the consumer.
65
+ - **Prep / vanish / cleanup hooks.** Detecting a disappeared worktree,
66
+ refusing cleanup, and running merge-cleanup tooling stay in adaptive-task.
67
+ - **`git worktree add` / `remove` as a product operation.** This crate
68
+ resolves locations and validates state. The consumer mutates git.
69
+
70
+ Using Codex Desktop only as an editor does not require importing this
71
+ package. Running adaptive-task identity, binding, or path policy still does.
72
+ Agent isolation worktrees and session worktrees can nest. Config may move
73
+ the session base when `CODEX_CLI` or `CLAUDE_CODE` is set. That override
74
+ exists because the two layers meet; it does not make this library redundant.
75
+
76
+ ## Architecture
77
+
78
+ ```mermaid
79
+ flowchart LR
80
+ callers["consumers"] -->|"worktree_runtime.api"| shim["Python shim and stubs"]
81
+ shim --> pyo3["PyO3 binding"]
82
+ pyo3 --> core["Rust core"]
83
+ contract["contract pytest"] -->|"black-box"| shim
84
+ cargo["cargo test"] --> core
85
+ release["maturin wheels"] --> callers
86
+ ```
87
+
88
+ ## Layout
89
+
90
+ Layout rationale is in [DESIGN.md](DESIGN.md).
91
+
92
+ - `crates/worktree_runtime_core/` — behavior SSOT
93
+ - `crates/worktree_runtime_pyo3/` — Python C-API translation only
94
+ - `src/worktree_runtime/` — `api` re-export, `.pyi`, `SURFACE.yaml`, `presentation/audit_cli.py`
95
+ - `src/_worktree_runtime_rust/` — placeholder package that hosts the cdylib built by maturin (`python-source = "src"`)
96
+ - `tests/contract/` — `from worktree_runtime.api import ...` only; `tests/cli/` — audit CLI exit codes
97
+
98
+ Distribution name is `worktree-runtime`. Import name is `worktree_runtime`.
99
+ `pyproject.toml` version and `Cargo.toml` `workspace.package.version` must match.
100
+ The crate is proprietary (`LICENSE`; all rights reserved). CI uses the `stable` toolchain with rustfmt and clippy.
101
+
102
+ ## Change protocol
103
+
104
+ Multi-file, refactor, behavior, or core+pyo3+facade+contract work: run
105
+ `adaptive-gate` first ([AGENTS.md](AGENTS.md) Routing). Testing policy:
106
+ `.cursor/rules/testing.mdc`.
107
+
108
+ 1. Behavior: add a failing cargo execution test, then implement in
109
+ `worktree_runtime_core` and run `cargo test`.
110
+ 2. Public-surface shape only: update `SURFACE.yaml` and `.pyi`, plus a thin
111
+ contract assertion under `tests/contract/`. Do not put behavior suites in
112
+ pytest.
113
+ 3. Export through PyO3; keep the shim as a re-export.
114
+ 4. When the public surface changed, build a wheel and run the contract and
115
+ CLI pytest. `cargo fmt --check` and `clippy -D warnings` are required.
116
+
117
+ The verify loop (commands, hook setup, release steps) is defined once in
118
+ [BUILD.md](BUILD.md).
119
+
120
+ ## Build
121
+
122
+ See [BUILD.md](BUILD.md). CI always runs fmt, clippy, cargo test, maturin,
123
+ contract+CLI pytest, `mypy.stubtest`, and an MSRV check, through
124
+ `.github/actions/rust-checks`.
125
+ Wheels are `abi3-py310` (CPython 3.10+) for Windows amd64, macOS x86_64 and
126
+ arm64, and manylinux x86_64 / aarch64. Pure-Python wheels are not published.
127
+
@@ -0,0 +1,102 @@
1
+ # worktree-runtime
2
+
3
+ Library for worktree **identity**, **binding persistence**, **checkout
4
+ validation**, and **path resolution**.
5
+
6
+ Consumers (adaptive-task / Noveler) call this so they do not each invent
7
+ project id, session↔worktree binding, main-vs-linked checks, or
8
+ state / shared paths. The public symbol list is
9
+ [`src/worktree_runtime/SURFACE.yaml`](src/worktree_runtime/SURFACE.yaml).
10
+
11
+ Binding store API: `register_binding`, `load_binding`, `recover_binding`,
12
+ `list_bindings`, and `remove_binding`. `git worktree remove` and vanish stay
13
+ in the consumer.
14
+
15
+ Rust is the implementation SSOT. Python is a shim, typing stubs, contract tests,
16
+ and a thin audit CLI. Missing native support is an install error; there is no
17
+ silent Python fallback in this project.
18
+
19
+ ## Scope
20
+
21
+ This crate answers four questions and records consumer mutations:
22
+
23
+ | Responsibility | What the library does |
24
+ | --- | --- |
25
+ | Identity | Resolve project id, main branches, GitHub policy, and the `NOVELER_*` / location env catalog from cwd, env, git, and project config. |
26
+ | Binding | Persist, load, recover, and validate a session↔worktree record (`WorktreeBinding`). Reject before treating a cwd as bound. |
27
+ | Checkout | Resolve a `GitWorktree` (`rev-parse`), classify main vs linked, list porcelain entries, emit reason codes. |
28
+ | Path | Resolve worktree base / dir / state dir / known bases with fixed precedence. `NOVELER_WORKTREE_BASE` and `config/worktree.yaml` overrides apply only when `CI` / `GITHUB_ACTIONS` is set; elsewhere the base is `~/.adaptive-task/worktrees/<project_id>`. Normalize and fingerprint paths. |
29
+
30
+ Audit (`record_event` / `read_records` / rotation, plus
31
+ `presentation/audit_cli.py`) is append-only logging of consumer git
32
+ mutations (`worktree_remove`, `branch_delete`). It is not a session log.
33
+
34
+ ### Out of scope
35
+
36
+ - **Agent isolation.** Codex Desktop, Cursor, and Claude Code create a
37
+ worktree (or checkout) so one chat does not dirty the user's main tree.
38
+ That sandbox is a different layer. This package does not replace it.
39
+ - **Session orchestration.** `adaptive-task init` / `next` / `report` stay
40
+ in the consumer.
41
+ - **Prep / vanish / cleanup hooks.** Detecting a disappeared worktree,
42
+ refusing cleanup, and running merge-cleanup tooling stay in adaptive-task.
43
+ - **`git worktree add` / `remove` as a product operation.** This crate
44
+ resolves locations and validates state. The consumer mutates git.
45
+
46
+ Using Codex Desktop only as an editor does not require importing this
47
+ package. Running adaptive-task identity, binding, or path policy still does.
48
+ Agent isolation worktrees and session worktrees can nest. Config may move
49
+ the session base when `CODEX_CLI` or `CLAUDE_CODE` is set. That override
50
+ exists because the two layers meet; it does not make this library redundant.
51
+
52
+ ## Architecture
53
+
54
+ ```mermaid
55
+ flowchart LR
56
+ callers["consumers"] -->|"worktree_runtime.api"| shim["Python shim and stubs"]
57
+ shim --> pyo3["PyO3 binding"]
58
+ pyo3 --> core["Rust core"]
59
+ contract["contract pytest"] -->|"black-box"| shim
60
+ cargo["cargo test"] --> core
61
+ release["maturin wheels"] --> callers
62
+ ```
63
+
64
+ ## Layout
65
+
66
+ Layout rationale is in [DESIGN.md](DESIGN.md).
67
+
68
+ - `crates/worktree_runtime_core/` — behavior SSOT
69
+ - `crates/worktree_runtime_pyo3/` — Python C-API translation only
70
+ - `src/worktree_runtime/` — `api` re-export, `.pyi`, `SURFACE.yaml`, `presentation/audit_cli.py`
71
+ - `src/_worktree_runtime_rust/` — placeholder package that hosts the cdylib built by maturin (`python-source = "src"`)
72
+ - `tests/contract/` — `from worktree_runtime.api import ...` only; `tests/cli/` — audit CLI exit codes
73
+
74
+ Distribution name is `worktree-runtime`. Import name is `worktree_runtime`.
75
+ `pyproject.toml` version and `Cargo.toml` `workspace.package.version` must match.
76
+ The crate is proprietary (`LICENSE`; all rights reserved). CI uses the `stable` toolchain with rustfmt and clippy.
77
+
78
+ ## Change protocol
79
+
80
+ Multi-file, refactor, behavior, or core+pyo3+facade+contract work: run
81
+ `adaptive-gate` first ([AGENTS.md](AGENTS.md) Routing). Testing policy:
82
+ `.cursor/rules/testing.mdc`.
83
+
84
+ 1. Behavior: add a failing cargo execution test, then implement in
85
+ `worktree_runtime_core` and run `cargo test`.
86
+ 2. Public-surface shape only: update `SURFACE.yaml` and `.pyi`, plus a thin
87
+ contract assertion under `tests/contract/`. Do not put behavior suites in
88
+ pytest.
89
+ 3. Export through PyO3; keep the shim as a re-export.
90
+ 4. When the public surface changed, build a wheel and run the contract and
91
+ CLI pytest. `cargo fmt --check` and `clippy -D warnings` are required.
92
+
93
+ The verify loop (commands, hook setup, release steps) is defined once in
94
+ [BUILD.md](BUILD.md).
95
+
96
+ ## Build
97
+
98
+ See [BUILD.md](BUILD.md). CI always runs fmt, clippy, cargo test, maturin,
99
+ contract+CLI pytest, `mypy.stubtest`, and an MSRV check, through
100
+ `.github/actions/rust-checks`.
101
+ Wheels are `abi3-py310` (CPython 3.10+) for Windows amd64, macOS x86_64 and
102
+ arm64, and manylinux x86_64 / aarch64. Pure-Python wheels are not published.
@@ -0,0 +1,55 @@
1
+ # worktree_runtime_core
2
+
3
+ This crate is the behavior SSOT. Do not add PyO3 or Python dependencies.
4
+ Behavior changes belong here with `cargo test` (`.cursor/rules/testing.mdc`);
5
+ do not treat pytest as the SSOT.
6
+
7
+ ## Layers
8
+
9
+ - `domain/` — value objects and pure rules (`binding`, `audit`, `paths`,
10
+ `porcelain`, `git_worktree`, `github_policy`). Today `paths::normalize_path`
11
+ still canonicalizes through the filesystem and `WorktreeBinding::new` calls
12
+ it; treat that as a known exception, do not add more I/O to `domain/`.
13
+ - `application/` — services that compose domain + infrastructure
14
+ (`binding_lifecycle`, `validation`, `git_worktree_service`). They own the
15
+ reason codes callers branch on.
16
+ - `infrastructure/` — filesystem, env, git subprocess, config readers.
17
+
18
+ ## Conventions that are not obvious from the code
19
+
20
+ - **git**: every subprocess goes through `infrastructure::git_adapter`.
21
+ `git_output` returns `Result<String, GitInvokeError>`; `Exit` means the
22
+ repository state is unusable (not a repo, detached HEAD), every other
23
+ variant means git itself could not run and callers report
24
+ `WORKTREE_GIT_UNAVAILABLE`. Do not collapse errors to `""`. Tests
25
+ simulate a missing binary with `common::MissingGit` (thread-local
26
+ `override_git_program`), never by clearing `PATH`.
27
+ - **Reason codes** are a public contract (adaptive-task branches on them).
28
+ Adding one needs a CHANGELOG entry; renaming one is a breaking change.
29
+ - **Binding store** (`infrastructure::binding_store`) is locked
30
+ (`bindings.json.lock`) and fail-closed: a corrupt file is an error, never
31
+ an empty store.
32
+ - **Audit** (`infrastructure::audit_*`) is fail-open by design: a failed
33
+ write prints `[WORKTREE-AUDIT-FAILOPEN] ...` to stderr and returns
34
+ `Err(AuditWriteError)`; the PyO3 layer decides whether to raise
35
+ (`strict`). Rotation runs under the append lock; `read_records` reads
36
+ archives too. The only `unsafe` in the crate is `libc::gethostname` in
37
+ `audit_writer.rs` (unix).
38
+ - **Config precedence**: `NOVELER_WORKTREE_BASE` and
39
+ `config/worktree.yaml` overrides are honored only when `CI` or
40
+ `GITHUB_ACTIONS` is set (`worktree_base_resolver.rs`). Outside CI the
41
+ base is `~/.adaptive-task/worktrees/<project_id>`. A missing
42
+ `config/worktree.yaml` is the default path. A file that exists but cannot
43
+ be read or parsed raises `WORKTREE_CONFIG_UNREADABLE` from the resolvers
44
+ that load it (`resolve_worktree_base`, `resolve_worktree_dir`,
45
+ `resolve_known_worktree_bases`) when CI consults it.
46
+ - **project_id**: a missing `pyproject.toml` or a missing
47
+ `tool.adaptive-task.project_id` is `noveler`. An existing file that is not
48
+ valid TOML raises `WORKTREE_PROJECT_CONFIG_UNREADABLE`. A declared id that
49
+ fails `^[a-z][a-z0-9_-]{0,63}$` raises `ProjectIdInvalidError`
50
+ (`PROJECT_ID_INVALID`) from `resolve_project_id` and
51
+ `resolve_state_dir_name`; it is not used as a directory name. An empty id
52
+ stays `noveler`.
53
+ - **Logging**: `log::debug!` / `log::warn!` for diagnostics; the two
54
+ `eprintln!` breadcrumbs (`failopen_message`, `suspicious_extra_message`)
55
+ are frozen by tests and stay on stderr.
@@ -0,0 +1,337 @@
1
+ //! Worktree binding lifecycle and current-binding validation.
2
+ use crate::application::validation::validate_binding;
3
+ use crate::domain::binding::{
4
+ BindingValidationOptions, BindingValidationResult, WorktreeBinding, WorktreeBindingFields,
5
+ };
6
+ use crate::domain::paths::normalize_path;
7
+ use crate::infrastructure::binding_store::{
8
+ resolve_store_root, BindingStoreError, WorktreeBindingStore,
9
+ };
10
+ use crate::infrastructure::env_keys::{NOVELER_BRANCH_ENV, NOVELER_WORKTREE_ENV};
11
+ use crate::infrastructure::git_adapter::{git_output, GitInvokeError};
12
+ use crate::infrastructure::project_id_resolver::{resolve_project_id, resolve_state_dir_name};
13
+ use serde_json::Value;
14
+ use std::collections::HashMap;
15
+ use std::fmt;
16
+ use std::path::{Path, PathBuf};
17
+
18
+ /// Why [`register_binding`] refused or failed. `Display` renders
19
+ /// `<REASON_CODE>: <message>` for both variants.
20
+ #[derive(Debug)]
21
+ pub enum RegisterBindingError {
22
+ /// The worktree / git state did not validate; nothing was written.
23
+ Validation {
24
+ reason_code: String,
25
+ message: String,
26
+ },
27
+ /// Validation passed but the binding could not be persisted.
28
+ Store(BindingStoreError),
29
+ }
30
+
31
+ impl RegisterBindingError {
32
+ pub fn reason_code(&self) -> &str {
33
+ match self {
34
+ RegisterBindingError::Validation { reason_code, .. } => reason_code,
35
+ RegisterBindingError::Store(err) => err.reason_code(),
36
+ }
37
+ }
38
+ }
39
+
40
+ impl fmt::Display for RegisterBindingError {
41
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42
+ match self {
43
+ RegisterBindingError::Validation {
44
+ reason_code,
45
+ message,
46
+ } => write!(f, "{reason_code}: {message}"),
47
+ RegisterBindingError::Store(err) => write!(f, "{err}"),
48
+ }
49
+ }
50
+ }
51
+
52
+ impl std::error::Error for RegisterBindingError {}
53
+
54
+ impl From<BindingStoreError> for RegisterBindingError {
55
+ fn from(err: BindingStoreError) -> Self {
56
+ RegisterBindingError::Store(err)
57
+ }
58
+ }
59
+
60
+ fn session_state_subpath(
61
+ repo_root: &Path,
62
+ ) -> Result<PathBuf, crate::infrastructure::project_id_resolver::ProjectResolveError> {
63
+ let pid = resolve_project_id(Some(&repo_root.to_string_lossy()))?;
64
+ Ok(PathBuf::from(resolve_state_dir_name(Some(&pid))?)
65
+ .join("state")
66
+ .join("adaptive-task"))
67
+ }
68
+
69
+ pub fn register_binding(
70
+ session_id: &str,
71
+ worktree_path: &str,
72
+ branch: &str,
73
+ repo_root: &str,
74
+ state_path: Option<&str>,
75
+ source_ref: &str,
76
+ ) -> Result<WorktreeBinding, RegisterBindingError> {
77
+ let worktree = normalize_path(worktree_path);
78
+ let repo = resolve_store_root(Path::new(&normalize_path(repo_root)));
79
+ let git_common_dir = match git_output(
80
+ Path::new(&worktree),
81
+ &["rev-parse", "--path-format=absolute", "--git-common-dir"],
82
+ ) {
83
+ Ok(dir) if !dir.is_empty() => dir,
84
+ Ok(_) | Err(GitInvokeError::Exit { .. }) => {
85
+ return Err(RegisterBindingError::Validation {
86
+ reason_code: "WORKTREE_GIT_COMMON_DIR_MISSING".to_string(),
87
+ message: "worktree git common dir is required".to_string(),
88
+ });
89
+ }
90
+ Err(err) => {
91
+ return Err(RegisterBindingError::Validation {
92
+ reason_code: crate::application::validation::REASON_GIT_UNAVAILABLE.to_string(),
93
+ message: format!("git could not be run: {err}"),
94
+ });
95
+ }
96
+ };
97
+ let state_path = if let Some(sp) = state_path {
98
+ normalize_path(sp)
99
+ } else {
100
+ let subpath = session_state_subpath(&repo).map_err(|err| match err {
101
+ crate::infrastructure::project_id_resolver::ProjectResolveError::Invalid {
102
+ message,
103
+ reason_code,
104
+ } => RegisterBindingError::Validation {
105
+ reason_code,
106
+ message,
107
+ },
108
+ other => RegisterBindingError::Store(other.into()),
109
+ })?;
110
+ normalize_path(&PathBuf::from(&worktree).join(&subpath).to_string_lossy())
111
+ };
112
+ let binding = WorktreeBinding::new(WorktreeBindingFields {
113
+ session_id: session_id.to_string(),
114
+ worktree: worktree.clone(),
115
+ branch: branch.to_string(),
116
+ state_path,
117
+ git_common_dir: normalize_path(&git_common_dir),
118
+ main_worktree: repo.to_string_lossy().to_string(),
119
+ commit_ish: source_ref.to_string(),
120
+ schema_version: None,
121
+ });
122
+ let mut env = HashMap::new();
123
+ env.insert(NOVELER_WORKTREE_ENV.to_string(), worktree.clone());
124
+ let validation = validate_binding(&binding, Some(&worktree), Some(&env), true, None);
125
+ if !validation.ok {
126
+ return Err(RegisterBindingError::Validation {
127
+ reason_code: validation.reason_code,
128
+ message: validation.message,
129
+ });
130
+ }
131
+ let store = WorktreeBindingStore::new(&repo)?;
132
+ store.upsert(&binding)?;
133
+ Ok(binding)
134
+ }
135
+
136
+ pub fn load_binding(
137
+ session_id: &str,
138
+ repo_root: &str,
139
+ ) -> Result<Option<WorktreeBinding>, BindingStoreError> {
140
+ WorktreeBindingStore::new(repo_root)?.load(session_id)
141
+ }
142
+
143
+ pub fn list_bindings(repo_root: &str) -> Result<Vec<WorktreeBinding>, BindingStoreError> {
144
+ WorktreeBindingStore::new(repo_root)?.list()
145
+ }
146
+
147
+ pub fn remove_binding(
148
+ session_id: &str,
149
+ repo_root: &str,
150
+ ) -> Result<Option<WorktreeBinding>, BindingStoreError> {
151
+ WorktreeBindingStore::new(repo_root)?.remove(session_id)
152
+ }
153
+
154
+ /// Rebuild a binding from legacy `metadata.json` and persist it. `Ok(None)`
155
+ /// means there was nothing usable to recover; `Err` means the store itself is
156
+ /// unavailable.
157
+ pub fn recover_binding(
158
+ session_id: &str,
159
+ repo_root: &str,
160
+ ) -> Result<Option<WorktreeBinding>, BindingStoreError> {
161
+ let candidate = normalize_path(repo_root);
162
+ let repo = resolve_store_root(Path::new(&candidate));
163
+ if let Some(binding) = recover_binding_from_metadata(session_id, Path::new(&candidate), &repo)?
164
+ {
165
+ return Ok(Some(binding));
166
+ }
167
+ if candidate != repo.to_string_lossy() {
168
+ return recover_binding_from_metadata(session_id, &repo, &repo);
169
+ }
170
+ Ok(None)
171
+ }
172
+
173
+ fn recover_binding_from_metadata(
174
+ session_id: &str,
175
+ metadata_root: &Path,
176
+ repo_root: &Path,
177
+ ) -> Result<Option<WorktreeBinding>, BindingStoreError> {
178
+ let state_subpath = session_state_subpath(repo_root)?;
179
+ let metadata = metadata_root
180
+ .join(state_subpath)
181
+ .join("sessions")
182
+ .join(session_id)
183
+ .join("metadata.json");
184
+ if !metadata.is_file() {
185
+ return Ok(None);
186
+ }
187
+ let Ok(content) = std::fs::read_to_string(&metadata) else {
188
+ return Ok(None);
189
+ };
190
+ let Ok(payload) = serde_json::from_str::<Value>(&content) else {
191
+ return Ok(None);
192
+ };
193
+ let Some(obj) = payload.as_object() else {
194
+ return Ok(None);
195
+ };
196
+ let Some(worktree_path) = obj.get("worktree_path").and_then(|v| v.as_str()) else {
197
+ return Ok(None);
198
+ };
199
+ let Some(branch) = obj
200
+ .get("branch")
201
+ .and_then(|v| v.as_str())
202
+ .or_else(|| obj.get("branch_name").and_then(|v| v.as_str()))
203
+ else {
204
+ return Ok(None);
205
+ };
206
+ if worktree_path.trim().is_empty() {
207
+ return Ok(None);
208
+ }
209
+ let source_ref = obj.get("source_ref").and_then(|v| v.as_str()).unwrap_or("");
210
+ match register_binding(
211
+ session_id,
212
+ worktree_path,
213
+ branch,
214
+ &repo_root.to_string_lossy(),
215
+ None,
216
+ source_ref,
217
+ ) {
218
+ Ok(binding) => Ok(Some(binding)),
219
+ Err(RegisterBindingError::Store(err)) => Err(err),
220
+ Err(RegisterBindingError::Validation {
221
+ reason_code,
222
+ message,
223
+ }) => {
224
+ log::debug!(
225
+ "recover_binding({session_id}): metadata {} rejected: {reason_code}: {message}",
226
+ metadata.display()
227
+ );
228
+ Ok(None)
229
+ }
230
+ }
231
+ }
232
+
233
+ /// Arguments for [`validate_current_binding`], grouped to avoid a
234
+ /// too-many-arguments positional signature.
235
+ pub struct BindingValidationRequest<'a> {
236
+ pub repo_root: &'a str,
237
+ pub session_id: Option<&'a str>,
238
+ pub cwd: &'a str,
239
+ pub env: Option<&'a HashMap<String, String>>,
240
+ pub check_git: bool,
241
+ pub require_registered_binding: bool,
242
+ pub require_env_worktree: bool,
243
+ pub require_git_state: bool,
244
+ }
245
+
246
+ fn store_unavailable(err: BindingStoreError) -> BindingValidationResult {
247
+ BindingValidationResult {
248
+ ok: false,
249
+ reason_code: err.reason_code().to_string(),
250
+ message: err.to_string(),
251
+ binding: None,
252
+ }
253
+ }
254
+
255
+ fn env_missing() -> BindingValidationResult {
256
+ BindingValidationResult {
257
+ ok: false,
258
+ reason_code: "WORKTREE_ENV_MISSING".to_string(),
259
+ message: format!("{} is required for this validation", NOVELER_WORKTREE_ENV),
260
+ binding: None,
261
+ }
262
+ }
263
+
264
+ pub fn validate_current_binding(request: BindingValidationRequest) -> BindingValidationResult {
265
+ let options = BindingValidationOptions {
266
+ require_env_worktree: request.require_env_worktree,
267
+ require_git_state: request.require_git_state,
268
+ };
269
+ let binding = match request.session_id {
270
+ Some(sid) => match load_binding(sid, request.repo_root) {
271
+ Ok(Some(b)) => b,
272
+ Ok(None) => match recover_binding(sid, request.repo_root) {
273
+ Ok(Some(b)) => b,
274
+ Ok(None) if request.require_registered_binding => {
275
+ return BindingValidationResult {
276
+ ok: false,
277
+ reason_code: "WORKTREE_BINDING_MISSING".to_string(),
278
+ message: "no worktree binding found for session".to_string(),
279
+ binding: None,
280
+ };
281
+ }
282
+ Ok(None) => match build_env_fallback_binding(sid, request.env, request.repo_root) {
283
+ Some(b) => b,
284
+ None => return env_missing(),
285
+ },
286
+ Err(err) => return store_unavailable(err),
287
+ },
288
+ Err(err) => return store_unavailable(err),
289
+ },
290
+ None => match build_env_fallback_binding("", request.env, request.repo_root) {
291
+ Some(b) => b,
292
+ None => return env_missing(),
293
+ },
294
+ };
295
+ validate_binding(
296
+ &binding,
297
+ Some(request.cwd),
298
+ request.env,
299
+ request.check_git,
300
+ Some(&options),
301
+ )
302
+ }
303
+
304
+ fn build_env_fallback_binding(
305
+ session_id: &str,
306
+ env: Option<&HashMap<String, String>>,
307
+ repo_root: &str,
308
+ ) -> Option<WorktreeBinding> {
309
+ let env = env?;
310
+ let env_worktree = env
311
+ .get(NOVELER_WORKTREE_ENV)
312
+ .map(|s| s.trim().to_string())
313
+ .unwrap_or_default();
314
+ if env_worktree.is_empty() {
315
+ return None;
316
+ }
317
+ let normalized_worktree = normalize_path(&env_worktree);
318
+ let repo = resolve_store_root(Path::new(&normalized_worktree));
319
+ let branch = env
320
+ .get(NOVELER_BRANCH_ENV)
321
+ .map(|s| s.to_string())
322
+ .unwrap_or_default();
323
+ let state_path = PathBuf::from(&normalized_worktree)
324
+ .join(session_state_subpath(&repo).ok()?)
325
+ .to_string_lossy()
326
+ .to_string();
327
+ Some(WorktreeBinding::new(WorktreeBindingFields {
328
+ session_id: session_id.to_string(),
329
+ worktree: normalized_worktree,
330
+ branch,
331
+ state_path,
332
+ git_common_dir: String::new(),
333
+ main_worktree: normalize_path(repo_root),
334
+ commit_ish: "env-fallback".to_string(),
335
+ schema_version: None,
336
+ }))
337
+ }