froid-loop 0.11.1__py3-none-any.whl

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 (116) hide show
  1. froid_loop/__init__.py +11 -0
  2. froid_loop/__main__.py +12 -0
  3. froid_loop/adapters/__init__.py +3 -0
  4. froid_loop/adapters/base.py +254 -0
  5. froid_loop/adapters/entrypoints.py +63 -0
  6. froid_loop/adapters/env_fault.py +290 -0
  7. froid_loop/adapters/generic.py +2013 -0
  8. froid_loop/adapters/mock.py +49 -0
  9. froid_loop/adapters/multiplexer.py +914 -0
  10. froid_loop/adapters/opencode_http.py +1687 -0
  11. froid_loop/adapters/profile.py +650 -0
  12. froid_loop/adapters/psmux_backend.py +1428 -0
  13. froid_loop/adapters/registry.py +322 -0
  14. froid_loop/adapters/tmux_backend.py +35 -0
  15. froid_loop/adapters/tmux_base.py +630 -0
  16. froid_loop/checks.py +187 -0
  17. froid_loop/cli.py +5041 -0
  18. froid_loop/data/__init__.py +0 -0
  19. froid_loop/data/froid_loop_hook.py +228 -0
  20. froid_loop/data/froid_loop_probe_hook.py +88 -0
  21. froid_loop/data/plugins/example/plugin.toml +21 -0
  22. froid_loop/data/plugins/tea/plugin.toml +184 -0
  23. froid_loop/data/plugins/tea/tea_plugin.py +258 -0
  24. froid_loop/data/plugins/unity/plugin.toml +140 -0
  25. froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef +16 -0
  26. froid_loop/data/plugins/unity/unity_assets/FroidLoop.Unity.Editor.asmdef.meta +7 -0
  27. froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs +221 -0
  28. froid_loop/data/plugins/unity/unity_assets/SceneAutoSaveGuard.cs.meta +11 -0
  29. froid_loop/data/plugins/unity/unity_assets/_folders/Editor.meta +8 -0
  30. froid_loop/data/plugins/unity/unity_assets/_folders/FroidLoop.meta +8 -0
  31. froid_loop/data/plugins/unity/unity_cleanup.py +125 -0
  32. froid_loop/data/plugins/unity/unity_dialog_probe.py +239 -0
  33. froid_loop/data/plugins/unity/unity_facts.md +17 -0
  34. froid_loop/data/plugins/unity/unity_plugin.py +415 -0
  35. froid_loop/data/plugins/unity/unity_quiesce.py +234 -0
  36. froid_loop/data/plugins/unity/unity_ready.py +230 -0
  37. froid_loop/data/plugins/unity/unity_seed_assets.py +298 -0
  38. froid_loop/data/plugins/unity/unity_setup.py +551 -0
  39. froid_loop/data/plugins/unity/unity_teardown.py +362 -0
  40. froid_loop/data/profiles/antigravity.toml +52 -0
  41. froid_loop/data/profiles/claude.toml +85 -0
  42. froid_loop/data/profiles/codex.toml +22 -0
  43. froid_loop/data/profiles/copilot.toml +52 -0
  44. froid_loop/data/profiles/gemini.toml +26 -0
  45. froid_loop/data/profiles/opencode.toml +54 -0
  46. froid_loop/data/settings/core.toml +458 -0
  47. froid_loop/data/skills/README.md +93 -0
  48. froid_loop/data/skills/froid-loop-resolve/SKILL.md +288 -0
  49. froid_loop/data/skills/froid-loop-setup/SKILL.md +161 -0
  50. froid_loop/data/skills/froid-loop-setup/assets/module-help.csv +3 -0
  51. froid_loop/data/skills/froid-loop-setup/assets/module.yaml +19 -0
  52. froid_loop/data/skills/froid-loop-sweep/SKILL.md +100 -0
  53. froid_loop/data/skills/froid-loop-sweep/automation-mode.md +127 -0
  54. froid_loop/data/skills/froid-loop-sweep/deferred-work-format.md +302 -0
  55. froid_loop/data/skills/froid-loop-sweep/migration-mode.md +86 -0
  56. froid_loop/decisions.py +202 -0
  57. froid_loop/deferredwork.py +2282 -0
  58. froid_loop/devcontract.py +892 -0
  59. froid_loop/diagnostics.py +1104 -0
  60. froid_loop/documents.py +532 -0
  61. froid_loop/engine.py +7732 -0
  62. froid_loop/envvars.py +111 -0
  63. froid_loop/escalation.py +225 -0
  64. froid_loop/events.py +266 -0
  65. froid_loop/fences.py +103 -0
  66. froid_loop/froidconfig.py +226 -0
  67. froid_loop/frontmatter.py +526 -0
  68. froid_loop/gates.py +133 -0
  69. froid_loop/install.py +2936 -0
  70. froid_loop/journal.py +178 -0
  71. froid_loop/machine.py +148 -0
  72. froid_loop/model.py +898 -0
  73. froid_loop/operatoractions.py +474 -0
  74. froid_loop/platform_util.py +1490 -0
  75. froid_loop/plugins/__init__.py +64 -0
  76. froid_loop/plugins/bus.py +259 -0
  77. froid_loop/plugins/context.py +319 -0
  78. froid_loop/plugins/loader.py +145 -0
  79. froid_loop/plugins/manifest.py +279 -0
  80. froid_loop/plugins/model.py +296 -0
  81. froid_loop/plugins/registry.py +245 -0
  82. froid_loop/plugins/trust.py +75 -0
  83. froid_loop/policy.py +1569 -0
  84. froid_loop/probe.py +1044 -0
  85. froid_loop/process_host.py +408 -0
  86. froid_loop/recovery_flow.py +1561 -0
  87. froid_loop/resolve.py +283 -0
  88. froid_loop/runs.py +4715 -0
  89. froid_loop/runsetup.py +1293 -0
  90. froid_loop/sanitize.py +593 -0
  91. froid_loop/settings_schema.py +276 -0
  92. froid_loop/signals.py +160 -0
  93. froid_loop/sprintstatus.py +609 -0
  94. froid_loop/statemachine.py +57 -0
  95. froid_loop/stories.py +615 -0
  96. froid_loop/stories_engine.py +796 -0
  97. froid_loop/sweep.py +1892 -0
  98. froid_loop/tokens.py +196 -0
  99. froid_loop/tui/__init__.py +11 -0
  100. froid_loop/tui/app.py +1584 -0
  101. froid_loop/tui/data.py +840 -0
  102. froid_loop/tui/launch.py +1003 -0
  103. froid_loop/tui/screens/__init__.py +1 -0
  104. froid_loop/tui/screens/dashboard.py +1071 -0
  105. froid_loop/tui/screens/modals.py +943 -0
  106. froid_loop/tui/screens/settings_screen.py +477 -0
  107. froid_loop/tui/settings.py +135 -0
  108. froid_loop/tui/widgets.py +981 -0
  109. froid_loop/verify.py +4545 -0
  110. froid_loop/workspace.py +320 -0
  111. froid_loop/worktree_flow.py +2301 -0
  112. froid_loop-0.11.1.dist-info/METADATA +728 -0
  113. froid_loop-0.11.1.dist-info/RECORD +116 -0
  114. froid_loop-0.11.1.dist-info/WHEEL +4 -0
  115. froid_loop-0.11.1.dist-info/entry_points.txt +2 -0
  116. froid_loop-0.11.1.dist-info/licenses/LICENSE +30 -0
@@ -0,0 +1,322 @@
1
+ """Coding-CLI adapter registry — the out-of-tree extension seam for the CLI axis.
2
+
3
+ The transport axis (:mod:`~.multiplexer`) has long been extensible out-of-tree:
4
+ a backend registers through ``register_multiplexer`` and a co-installed package
5
+ is discovered via the ``froid_loop.mux_backends`` entry-point group. This module
6
+ is the same seam for the *other* axis — which adapter **class** drives a coding
7
+ CLI. A CLI that fits the tmux-injection + hook-signal transport still needs no
8
+ Python at all (drop a TOML :class:`~.profile.CLIProfile` and run ``froid-loop
9
+ probe-adapter``); this registry is for a CLI that needs a whole new adapter
10
+ subclass (the shipped example is the HTTP/SSE ``opencode-http`` adapter, which no
11
+ tmux profile can host).
12
+
13
+ An adapter *kind* is selected by **data**: ``profile.adapter`` names the kind, and
14
+ :func:`get_adapter_kind` resolves it. Two registration-time dataclasses carry the
15
+ family:
16
+
17
+ - :class:`AdapterKind` — ``name`` + ``needs_mux`` (does the family drive a
18
+ terminal multiplexer?) + ``load``, a lazy thunk returning the builder. The thunk
19
+ is why registration, validation (``known_adapter_kinds``) and listing
20
+ (``detect_adapters``) never import a heavy adapter module — nor an optional
21
+ dependency like ``httpx``, which only the opencode family pulls in at
22
+ construction.
23
+ - :class:`AdapterBuilder` — the ``plain`` class, the ``_DevSynthesisMixin``-composed
24
+ ``dev`` class (both share the ``(*args, paths, **kwargs)`` dev ``__init__``), and
25
+ the family's construction-failure exception type(s) (``()`` = none;
26
+ ``(OpencodeServerError,)`` for the HTTP family, which fails loud when its server
27
+ can't spawn). ``runsetup.make_adapters`` converts a raised ``construct_error``
28
+ into a ``SystemExit``.
29
+
30
+ Bundled kinds register from :func:`_load_builtin_adapters` (:data:`GENERIC`,
31
+ :data:`OPENCODE_HTTP`); out-of-tree kinds arrive at import time, triggered by the
32
+ ``froid_loop.adapters`` entry-point scan in :func:`_load_external_adapters` — so a
33
+ pip/uv co-installed adapter package is selectable with no config step. Builtins
34
+ are seeded by :func:`register_adapter` itself, so an external can never shadow a
35
+ bundled name however early its import lands. A broken third-party distribution
36
+ degrades to a recorded, surfaced reason (:func:`external_adapter_errors`) and can
37
+ never break selection — one reason per failing distribution, kept even when two of
38
+ them advertise the same entry-point name (:mod:`~.entrypoints`).
39
+
40
+ **Two deliberate asymmetries versus the multiplexer seam** (this is not a
41
+ copy-paste omission):
42
+
43
+ - *No process-wide cache / no ``cache_clear``.* The multiplexer is a single
44
+ process-wide singleton behind an ``lru_cache``; adapters are built **per run**
45
+ in ``runsetup.make_adapters``, which keeps its own ``by_cfg`` cache keyed on
46
+ ``(resolved-config, synthesizes)``. Selection here is a pure registry lookup, so
47
+ there is nothing to cache and :func:`register_adapter` invalidates nothing.
48
+ - *No ``configure_*`` / ``matches(platform)`` / platform defaults.* The multiplexer
49
+ is chosen by a policy knob and a ``sys.platform`` predicate; an adapter kind is
50
+ chosen by the ``profile.adapter`` field alone. There is no host-dependent
51
+ auto-selection and no persisted choice to install, so none of that machinery
52
+ exists — ``get_adapter_kind(name)`` fails loud on an unknown name rather than
53
+ falling back.
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import importlib.metadata
59
+ from collections.abc import Callable
60
+ from dataclasses import dataclass
61
+
62
+ from .entrypoints import record_load_error
63
+
64
+ # The two bundled kind names, as constants rather than literals scattered across
65
+ # modules. `validate`'s httpx check keys on OPENCODE_HTTP because httpx is *that
66
+ # family's* optional extra — a fact about one bundled family, which is a different
67
+ # thing from the set of VALID kinds (that set is only ever `known_adapter_kinds()`,
68
+ # never a literal). GENERIC is the `profile.adapter` default.
69
+ GENERIC = "generic"
70
+ OPENCODE_HTTP = "opencode-http"
71
+
72
+
73
+ class AdapterError(Exception):
74
+ """An adapter kind could not be resolved (an unknown ``profile.adapter``).
75
+
76
+ Construction failures a *known* family raises during ``__init__`` are its own
77
+ types (e.g. ``OpencodeServerError``), carried by
78
+ :attr:`AdapterBuilder.construct_error`, not this seam-level type."""
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class AdapterBuilder:
83
+ """The classes and failure modes of one adapter family.
84
+
85
+ ``plain`` and ``dev`` are the two variants ``runsetup.make_adapters`` picks
86
+ between on the ``synthesizes`` axis (a ``froid-build-auto`` dev/review session
87
+ gets ``dev``, which takes an extra ``paths=`` kwarg; every other role gets
88
+ ``plain``). ``construct_error`` is the tuple of exception types the family's
89
+ constructor raises when the session cannot be built — empty for a family that
90
+ cannot fail construction (``generic``); the caller wraps a match into a
91
+ ``SystemExit`` so a run aborts with a clean message instead of a traceback."""
92
+
93
+ plain: type
94
+ dev: type
95
+ construct_error: tuple[type[BaseException], ...] = ()
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class AdapterKind:
100
+ """One registered adapter family, keyed by ``name`` (the ``profile.adapter``
101
+ value). ``needs_mux`` gates whether ``runsetup.make_adapters`` resolves and
102
+ usability-checks the shared terminal multiplexer for this family (a hookless
103
+ HTTP/SSE family needs no transport). ``load`` is a lazy thunk returning the
104
+ :class:`AdapterBuilder`; it is the *only* place the family's classes (and any
105
+ optional dependency they pull in) are imported."""
106
+
107
+ name: str
108
+ needs_mux: bool
109
+ load: Callable[[], AdapterBuilder]
110
+
111
+
112
+ # ---------------------------------------------------------------- builtins
113
+
114
+
115
+ def _generic_builder() -> AdapterBuilder:
116
+ from .generic import GenericAdapter, GenericDevAdapter
117
+
118
+ return AdapterBuilder(plain=GenericAdapter, dev=GenericDevAdapter, construct_error=())
119
+
120
+
121
+ def _opencode_http_builder() -> AdapterBuilder:
122
+ from .opencode_http import (
123
+ OpencodeDevAdapter,
124
+ OpencodeHttpAdapter,
125
+ OpencodeServerError,
126
+ )
127
+
128
+ return AdapterBuilder(
129
+ plain=OpencodeHttpAdapter,
130
+ dev=OpencodeDevAdapter,
131
+ construct_error=(OpencodeServerError,),
132
+ )
133
+
134
+
135
+ # The bundled kinds, as (name, needs_mux, load-thunk). A module constant, not
136
+ # mutable registry state, so detect_adapters can label a row builtin-vs-external
137
+ # without the fixtures having to snapshot it. `generic` drives tmux + hooks and
138
+ # needs the multiplexer; `opencode-http` is hookless HTTP/SSE and does not.
139
+ _BUILTIN_ADAPTERS: tuple[tuple[str, bool, Callable[[], AdapterBuilder]], ...] = (
140
+ (GENERIC, True, _generic_builder),
141
+ (OPENCODE_HTTP, False, _opencode_http_builder),
142
+ )
143
+ _BUILTIN_NAMES = frozenset(name for name, _, _ in _BUILTIN_ADAPTERS)
144
+
145
+ # The live registry: name -> AdapterKind. Unlike the multiplexer's ordered list
146
+ # (registration order breaks selection ties), adapter selection is a pure by-name
147
+ # lookup, so a dict with first-registration-wins semantics is the whole story.
148
+ _ADAPTERS: dict[str, AdapterKind] = {}
149
+ _BUILTINS_LOADED = False
150
+
151
+
152
+ def register_adapter(name: str, needs_mux: bool, load: Callable[[], AdapterBuilder]) -> None:
153
+ """Register an adapter kind. ``name`` is the ``profile.adapter`` key that
154
+ selects it; ``needs_mux`` declares whether the family drives a terminal
155
+ multiplexer; ``load`` is the lazy builder thunk. First registration of a name
156
+ wins, and the builtins are seeded here rather than only by the resolution
157
+ entry points, so an out-of-tree package can never shadow a bundled name. An
158
+ out-of-tree kind calls this at import time — no core edit required. There is
159
+ no selection cache to invalidate (see the module docstring).
160
+
161
+ Seeding on *this* side is what makes first-wins an invariant instead of an
162
+ ordering coincidence. An external module runs its ``register_adapter`` calls
163
+ as an import side effect, and the import is not always triggered by an
164
+ adapter resolution: the documented packaging layout puts both entry points in
165
+ one module, so the ``froid_loop.profiles`` scan in :mod:`~.profile` — which
166
+ runs long before any kind is resolved — imports it too, as does any plugin
167
+ that imports the package directly. Any of those arriving first would have
168
+ ``setdefault`` keep the external under a bundled name and silently redirect
169
+ every default profile to it."""
170
+ _load_builtin_adapters()
171
+ _ADAPTERS.setdefault(name, AdapterKind(name=name, needs_mux=needs_mux, load=load))
172
+
173
+
174
+ def _load_builtin_adapters() -> None:
175
+ """Register the bundled adapter kinds. Idempotent and lazy (called from the
176
+ resolution entry points and from :func:`register_adapter`, not at module
177
+ import) to stay cycle-safe: the load thunks import ``generic`` /
178
+ ``opencode_http``, which import back through the package. Builtins register
179
+ before externals so a bundled name keeps first-wins on any collision.
180
+
181
+ The flag is set BEFORE the loop because the loop re-enters through
182
+ ``register_adapter``; setting it afterwards would recurse without end."""
183
+ global _BUILTINS_LOADED
184
+ if _BUILTINS_LOADED:
185
+ return
186
+ _BUILTINS_LOADED = True
187
+ for name, needs_mux, load in _BUILTIN_ADAPTERS:
188
+ register_adapter(name, needs_mux, load)
189
+
190
+
191
+ # The entry-point group an out-of-tree adapter package advertises its module
192
+ # under; importing the module runs its register_adapter call. Loader state:
193
+ # scanned-once flag + per-entry-point failure reasons for adapters/validate.
194
+ ADAPTERS_GROUP = "froid_loop.adapters"
195
+ _EXTERNALS_LOADED = False
196
+ _EXTERNAL_ERRORS: dict[str, str] = {}
197
+
198
+
199
+ def _load_external_adapters() -> None:
200
+ """Import every ``froid_loop.adapters`` entry point; each module self-registers
201
+ via :func:`register_adapter` at import time. Called after
202
+ :func:`_load_builtin_adapters`, so builtins keep first registration.
203
+
204
+ A broken third-party distribution must never break adapter selection:
205
+ failures are recorded in ``_EXTERNAL_ERRORS`` (surfaced by ``froid-loop
206
+ adapters`` and the ``validate`` preflight via :func:`external_adapter_errors`),
207
+ not raised. The loaded-flag is set up front: a third-party import failure is
208
+ not transient, and retrying on every resolution would re-import (and re-fail)
209
+ each time — mirroring the multiplexer's external scan.
210
+
211
+ A recorded failure does NOT mean the entry point registered nothing: a module
212
+ that registers kind A and then raises while registering kind B leaves A
213
+ registered and selectable. Deliberate — unwinding would mean tracking which
214
+ names a half-run import claimed, and a kind that registered cleanly is usable
215
+ whatever else its package got wrong. The recorded reason is a fact about the
216
+ import, not a promise about the registry.
217
+
218
+ Entry points are visited in (name, distribution) order. ``importlib.metadata``
219
+ yields them in distribution-discovery order, which varies with ``sys.path``, so
220
+ without an explicit sort two hosts carrying the same packages could resolve a
221
+ collision differently — first-wins would be a fact about the install rather
222
+ than about the packages.
223
+
224
+ The distribution belongs in the key because the name alone is NOT a total
225
+ order. ``entry_points(group=...)`` does not dedup across distributions, so two
226
+ packages advertising the same entry-point name come back as two entries, and
227
+ ``sorted`` is stable — a name-only key resolves that tie straight back into
228
+ ``sys.path`` order. That tie is the whole case the sort exists for: a package
229
+ conventionally names its entry point after the kind it registers, so packages
230
+ that collide on a kind normally collide on the entry-point name too."""
231
+ global _EXTERNALS_LOADED
232
+ if _EXTERNALS_LOADED:
233
+ return
234
+ _EXTERNALS_LOADED = True
235
+ try:
236
+ eps = sorted(
237
+ importlib.metadata.entry_points(group=ADAPTERS_GROUP),
238
+ key=lambda e: (e.name, getattr(e.dist, "name", "") or ""),
239
+ )
240
+ except Exception as exc: # noqa: BLE001 — diagnostics path, never crash selection
241
+ _EXTERNAL_ERRORS["<entry-point scan>"] = f"{type(exc).__name__}: {exc}"
242
+ return
243
+ for ep in eps:
244
+ try:
245
+ ep.load() # module import runs register_adapter(...)
246
+ except Exception as exc: # noqa: BLE001 — one bad package must not hide the rest
247
+ record_load_error(_EXTERNAL_ERRORS, ep, exc)
248
+
249
+
250
+ def external_adapter_errors() -> dict[str, str]:
251
+ """Entry-point name -> failure reason(s) for every external adapter that failed
252
+ to load this process (empty when all loaded). For diagnostics surfaces.
253
+
254
+ One value may carry MORE than one reason, ``"; "``-joined: two distributions
255
+ may advertise the same entry-point name, and each of their failures is kept
256
+ (see :func:`~.entrypoints.record_load_error`). Each reason is labelled with
257
+ its distribution whenever one is resolvable.
258
+
259
+ Performs the scan itself rather than relying on a neighbouring
260
+ ``known_adapter_kinds`` / ``detect_adapters`` call having run first — an
261
+ accessor whose emptiness depends on call order reads as "nothing failed"."""
262
+ _load_builtin_adapters()
263
+ _load_external_adapters()
264
+ return dict(_EXTERNAL_ERRORS)
265
+
266
+
267
+ def _known() -> str:
268
+ return ", ".join(sorted(_ADAPTERS)) or "(none registered)"
269
+
270
+
271
+ def get_adapter_kind(name: str) -> AdapterKind:
272
+ """Resolve the adapter kind named ``name`` (a ``profile.adapter`` value),
273
+ loading the builtins and scanning the entry-point group first.
274
+
275
+ Fails loud on an unknown name, listing the registered kinds — an explicit but
276
+ unregistered adapter is a misconfiguration (a typo, or a plugin package that
277
+ isn't installed), never something to silently fall back from. The caller
278
+ (``runsetup.make_adapters``) adds the offending profile's name to the
279
+ message."""
280
+ _load_builtin_adapters()
281
+ _load_external_adapters()
282
+ kind = _ADAPTERS.get(name)
283
+ if kind is None:
284
+ raise AdapterError(f"unknown adapter kind {name!r}; known: {_known()}")
285
+ return kind
286
+
287
+
288
+ def known_adapter_kinds() -> list[str]:
289
+ """Sorted names of every registered adapter kind (builtins + successfully
290
+ loaded externals). The oracle for ``validate``'s ``adapter.kind`` finding —
291
+ validity of ``profile.adapter`` is enforced against this registry, never a
292
+ hardcoded set."""
293
+ _load_builtin_adapters()
294
+ _load_external_adapters()
295
+ return sorted(_ADAPTERS)
296
+
297
+
298
+ @dataclass(frozen=True)
299
+ class AdapterKindInfo:
300
+ """One registered adapter kind's detection row, for ``froid-loop adapters`` and
301
+ the ``validate`` preflight."""
302
+
303
+ name: str
304
+ needs_mux: bool
305
+ builtin: bool
306
+
307
+
308
+ def detect_adapters() -> list[AdapterKindInfo]:
309
+ """Enumerate every registered adapter kind (builtins + loaded externals),
310
+ sorted by name, each labelled builtin-vs-external. Never raises — this feeds
311
+ diagnostics, which must work on a misconfigured host. The ``load`` thunk is
312
+ never invoked, so listing stays free of any heavy adapter import."""
313
+ _load_builtin_adapters()
314
+ _load_external_adapters()
315
+ return [
316
+ AdapterKindInfo(
317
+ name=kind.name,
318
+ needs_mux=kind.needs_mux,
319
+ builtin=kind.name in _BUILTIN_NAMES,
320
+ )
321
+ for kind in sorted(_ADAPTERS.values(), key=lambda k: k.name)
322
+ ]
@@ -0,0 +1,35 @@
1
+ """POSIX tmux backend for the terminal-multiplexer seam.
2
+
3
+ The tmux/POSIX-shell quarantine spans this file and its base
4
+ (:mod:`.tmux_base`) — together they are the **only** place in the codebase
5
+ allowed to shell out to ``tmux``, so a future non-POSIX backend (an eventual
6
+ native-Windows "psmux") can replace them wholesale. All argv construction and
7
+ the single spawn primitive live in :class:`~.tmux_base.BaseTmuxBackend`; this
8
+ leaf is the POSIX implementation and inherits the full contract unchanged. See
9
+ :mod:`.multiplexer` for the contract.
10
+
11
+ ``subprocess`` and ``shutil`` are imported (and re-exported) here so existing
12
+ callers and tests can still reach the spawn seam via ``tmux_backend.subprocess``
13
+ / ``tmux_backend.shutil``; the live calls run through ``tmux_base``.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import shutil # noqa: F401 — re-exported for callers/tests reaching the spawn seam
19
+ import subprocess # noqa: F401 — re-exported for callers/tests reaching the spawn seam
20
+
21
+ from .tmux_base import PARKED_RETURN_DETACH # noqa: F401 — re-exported for back-compat
22
+ from .tmux_base import TMUX_TIMEOUT_S # noqa: F401 — re-exported for back-compat
23
+ from .tmux_base import TmuxError # noqa: F401 — re-exported for back-compat
24
+ from .tmux_base import (
25
+ BaseTmuxBackend,
26
+ )
27
+
28
+
29
+ class TmuxMultiplexer(BaseTmuxBackend):
30
+ """POSIX tmux backend — inherits the full contract from BaseTmuxBackend.
31
+
32
+ Registered by :func:`~.multiplexer._load_builtin_backends` (the bundled loader),
33
+ not at import time, so the registry can be cleared and re-loaded deterministically
34
+ in tests — mirroring how ``process_host._load_builtin_hosts`` registers its hosts.
35
+ """