workstate-protocol 0.1.5__tar.gz → 0.1.7__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.
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/PKG-INFO +1 -1
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/pyproject.toml +1 -1
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/__init__.py +19 -1
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/bootstrap.py +44 -6
- workstate_protocol-0.1.7/src/workstate_protocol/env_aliases.py +54 -0
- workstate_protocol-0.1.7/src/workstate_protocol/paths.py +48 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/PKG-INFO +1 -1
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/SOURCES.txt +1 -0
- workstate_protocol-0.1.7/tests/test_env_aliases.py +39 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_plugin_override_schema.py +1 -1
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_skill_manifest_real_skills.py +2 -2
- workstate_protocol-0.1.5/src/workstate_protocol/env_aliases.py +0 -97
- workstate_protocol-0.1.5/tests/test_env_aliases.py +0 -98
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/README.md +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/setup.cfg +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/branch_naming.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/compaction.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/handoff.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/hooks.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/py.typed +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/skills.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/dependency_links.txt +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/requires.txt +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/top_level.txt +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_bootstrap_manifest.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_branch_grammar_registry.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_branch_naming.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_handoff_schema.py +0 -0
- {workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_package_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workstate-protocol
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Typed cross-repo contracts for the Workstate system: handoff state, MCP I/O, hook events, skill manifests, bootstrap install manifest.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/darce/workstate
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "workstate-protocol"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
description = "Typed cross-repo contracts for the Workstate system: handoff state, MCP I/O, hook events, skill manifests, bootstrap install manifest."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -32,20 +32,36 @@ from .hooks import (
|
|
|
32
32
|
StopEvent,
|
|
33
33
|
UserPromptSubmitEvent,
|
|
34
34
|
)
|
|
35
|
+
from .paths import (
|
|
36
|
+
CONTRACTS_DIR,
|
|
37
|
+
DOCS_MIRROR_DIR,
|
|
38
|
+
HARNESS_CONTRACT_RELPATH,
|
|
39
|
+
INSTRUCTIONS_RELPATH,
|
|
40
|
+
RULES_DIR,
|
|
41
|
+
RUNTIME_ROOT_DIRNAME,
|
|
42
|
+
docs_mirror_path,
|
|
43
|
+
runtime_root_path,
|
|
44
|
+
)
|
|
35
45
|
from .skills import SkillManifest, SkillScope
|
|
36
46
|
|
|
37
|
-
__version__ = "0.1.
|
|
47
|
+
__version__ = "0.1.6"
|
|
38
48
|
|
|
39
49
|
__all__ = [
|
|
40
50
|
"ActiveTask",
|
|
41
51
|
"BootstrapManifest",
|
|
52
|
+
"CONTRACTS_DIR",
|
|
53
|
+
"DOCS_MIRROR_DIR",
|
|
42
54
|
"DecisionRef",
|
|
55
|
+
"HARNESS_CONTRACT_RELPATH",
|
|
43
56
|
"HandoffState",
|
|
44
57
|
"HandoffStatus",
|
|
58
|
+
"INSTRUCTIONS_RELPATH",
|
|
45
59
|
"OverlayConfigEntry",
|
|
46
60
|
"OverlaySurface",
|
|
47
61
|
"PostToolUseEvent",
|
|
48
62
|
"PreToolUseEvent",
|
|
63
|
+
"RULES_DIR",
|
|
64
|
+
"RUNTIME_ROOT_DIRNAME",
|
|
49
65
|
"SessionStartEvent",
|
|
50
66
|
"SkillManifest",
|
|
51
67
|
"SkillScope",
|
|
@@ -61,6 +77,8 @@ __all__ = [
|
|
|
61
77
|
"__version__",
|
|
62
78
|
"branch_naming",
|
|
63
79
|
"derive_task_ref_candidates",
|
|
80
|
+
"docs_mirror_path",
|
|
64
81
|
"format_suggested_branch_name",
|
|
65
82
|
"resolve_env_alias",
|
|
83
|
+
"runtime_root_path",
|
|
66
84
|
]
|
|
@@ -27,7 +27,9 @@ class OverlaySurface(BaseModel):
|
|
|
27
27
|
|
|
28
28
|
model_config = ConfigDict(extra="allow")
|
|
29
29
|
|
|
30
|
-
path: str = Field(
|
|
30
|
+
path: str = Field(
|
|
31
|
+
description="Repo-relative surface path (e.g. '.claude/skills/handoff-lifecycle')."
|
|
32
|
+
)
|
|
31
33
|
source: Literal["shared", "local", "overlapping", "generated", "lifecycle"] = Field(
|
|
32
34
|
description=(
|
|
33
35
|
"Origin tier: 'shared' (symlinked from workstate-system), "
|
|
@@ -112,7 +114,9 @@ class PluginOverrideManifest(BaseModel):
|
|
|
112
114
|
|
|
113
115
|
schema_version: Literal[1] = 1
|
|
114
116
|
plugin: PluginComponentName
|
|
115
|
-
components: PluginOverrideComponents = Field(
|
|
117
|
+
components: PluginOverrideComponents = Field(
|
|
118
|
+
default_factory=PluginOverrideComponents
|
|
119
|
+
)
|
|
116
120
|
|
|
117
121
|
|
|
118
122
|
class ReplaceCommandOp(BaseModel):
|
|
@@ -240,10 +244,24 @@ class BootstrapManifest(BaseModel):
|
|
|
240
244
|
model_config = ConfigDict(extra="allow")
|
|
241
245
|
|
|
242
246
|
schema_version: int = Field(ge=1, description="Manifest schema version.")
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
+
source_kind: Literal["git_overlay", "package"] = Field(
|
|
248
|
+
default="git_overlay",
|
|
249
|
+
description=(
|
|
250
|
+
"Overlay delivery source: 'git_overlay' (a clone checked out at "
|
|
251
|
+
"remote_ref) or 'package' (an installed workstate-system "
|
|
252
|
+
"distribution). Defaults to 'git_overlay' so manifests written "
|
|
253
|
+
"before WS-PKG-DELIVERY-01 validate unchanged."
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
remote_url: str | None = Field(default=None, min_length=1)
|
|
257
|
+
remote_ref: str | None = Field(default=None, min_length=1)
|
|
258
|
+
remote_sha: Sha40 | None = Field(
|
|
259
|
+
default=None,
|
|
260
|
+
description="Resolved 40-char git SHA at install time (git_overlay source).",
|
|
261
|
+
)
|
|
262
|
+
package_version: str | None = Field(
|
|
263
|
+
default=None,
|
|
264
|
+
description="Installed workstate-system distribution version (package source).",
|
|
247
265
|
)
|
|
248
266
|
surfaces: list[OverlaySurface] = Field(default_factory=list)
|
|
249
267
|
configs: list[OverlayConfigEntry] = Field(default_factory=list)
|
|
@@ -265,3 +283,23 @@ class BootstrapManifest(BaseModel):
|
|
|
265
283
|
"override location."
|
|
266
284
|
),
|
|
267
285
|
)
|
|
286
|
+
|
|
287
|
+
@model_validator(mode="after")
|
|
288
|
+
def _check_source_provenance(self) -> "BootstrapManifest":
|
|
289
|
+
"""Each delivery source requires its own provenance fields.
|
|
290
|
+
|
|
291
|
+
``git_overlay`` (the default, so pre-WS-PKG-DELIVERY-01 manifests keep
|
|
292
|
+
validating) requires the git ``remote_*`` triple; ``package`` requires
|
|
293
|
+
the installed distribution ``package_version``.
|
|
294
|
+
"""
|
|
295
|
+
if self.source_kind == "git_overlay":
|
|
296
|
+
missing = [
|
|
297
|
+
name
|
|
298
|
+
for name in ("remote_url", "remote_ref", "remote_sha")
|
|
299
|
+
if not getattr(self, name)
|
|
300
|
+
]
|
|
301
|
+
if missing:
|
|
302
|
+
raise ValueError("git_overlay manifest requires " + ", ".join(missing))
|
|
303
|
+
elif self.source_kind == "package" and not self.package_version:
|
|
304
|
+
raise ValueError("package manifest requires package_version")
|
|
305
|
+
return self
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Canonical Workstate env-var resolution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import overload
|
|
8
|
+
|
|
9
|
+
__all__ = ["resolve_env_alias"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _non_empty(value: str | None) -> str | None:
|
|
13
|
+
if value is None:
|
|
14
|
+
return None
|
|
15
|
+
stripped = value.strip()
|
|
16
|
+
return stripped or None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
def resolve_env_alias(
|
|
21
|
+
canonical: str,
|
|
22
|
+
*,
|
|
23
|
+
env: Mapping[str, str] | None = ...,
|
|
24
|
+
default: str,
|
|
25
|
+
) -> str: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@overload
|
|
29
|
+
def resolve_env_alias(
|
|
30
|
+
canonical: str,
|
|
31
|
+
*,
|
|
32
|
+
env: Mapping[str, str] | None = ...,
|
|
33
|
+
default: None = ...,
|
|
34
|
+
) -> str | None: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_env_alias(
|
|
38
|
+
canonical: str,
|
|
39
|
+
*,
|
|
40
|
+
env: Mapping[str, str] | None = None,
|
|
41
|
+
default: str | None = None,
|
|
42
|
+
) -> str | None:
|
|
43
|
+
"""Resolve a canonical Workstate env var.
|
|
44
|
+
|
|
45
|
+
Blank/whitespace-only values are treated as unset, matching the existing
|
|
46
|
+
``_first_non_empty_env`` behaviour in the handoff package.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
source = os.environ if env is None else env
|
|
50
|
+
|
|
51
|
+
canonical_value = _non_empty(source.get(canonical))
|
|
52
|
+
if canonical_value is not None:
|
|
53
|
+
return canonical_value
|
|
54
|
+
return default
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Canonical Workstate runtime + doc-mirror path roots — single source of truth.
|
|
2
|
+
|
|
3
|
+
The runtime install directory is ``.workstate/`` and the mirrored docs/contracts
|
|
4
|
+
path is ``docs/workstate/``. Every package resolves these through this module so
|
|
5
|
+
the names live in exactly one place: a future path change is a one-line flip
|
|
6
|
+
here, not a repo-wide sweep.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Runtime install root — bootstrap materializes overlay surfaces and the remote
|
|
14
|
+
# clone under ``<target>/.workstate/``.
|
|
15
|
+
RUNTIME_ROOT_DIRNAME = ".workstate"
|
|
16
|
+
|
|
17
|
+
# Mirrored docs / contracts path — the SHARED_SURFACES consumed at install time
|
|
18
|
+
# (rules, contracts, templates) live under ``docs/workstate/``.
|
|
19
|
+
DOCS_MIRROR_DIR = "docs/workstate"
|
|
20
|
+
|
|
21
|
+
# Common derived locations under the canonical docs mirror.
|
|
22
|
+
CONTRACTS_DIR = f"{DOCS_MIRROR_DIR}/contracts"
|
|
23
|
+
RULES_DIR = f"{DOCS_MIRROR_DIR}/rules"
|
|
24
|
+
HARNESS_CONTRACT_RELPATH = Path(CONTRACTS_DIR) / "harness-protocol.yaml"
|
|
25
|
+
INSTRUCTIONS_RELPATH = Path(DOCS_MIRROR_DIR) / "instructions.md"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"CONTRACTS_DIR",
|
|
29
|
+
"DOCS_MIRROR_DIR",
|
|
30
|
+
"HARNESS_CONTRACT_RELPATH",
|
|
31
|
+
"INSTRUCTIONS_RELPATH",
|
|
32
|
+
"RULES_DIR",
|
|
33
|
+
"RUNTIME_ROOT_DIRNAME",
|
|
34
|
+
"docs_mirror_path",
|
|
35
|
+
"runtime_root_path",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def docs_mirror_path(*parts: str) -> Path:
|
|
40
|
+
"""Return a path under the canonical docs mirror (``docs/workstate/...``)."""
|
|
41
|
+
|
|
42
|
+
return Path(DOCS_MIRROR_DIR, *parts)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def runtime_root_path(base: Path, *parts: str) -> Path:
|
|
46
|
+
"""Return a path under ``<base>/.workstate/...``."""
|
|
47
|
+
|
|
48
|
+
return base.joinpath(RUNTIME_ROOT_DIRNAME, *parts)
|
{workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workstate-protocol
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Typed cross-repo contracts for the Workstate system: handoff state, MCP I/O, hook events, skill manifests, bootstrap install manifest.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/darce/workstate
|
{workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/SOURCES.txt
RENAMED
|
@@ -7,6 +7,7 @@ src/workstate_protocol/compaction.py
|
|
|
7
7
|
src/workstate_protocol/env_aliases.py
|
|
8
8
|
src/workstate_protocol/handoff.py
|
|
9
9
|
src/workstate_protocol/hooks.py
|
|
10
|
+
src/workstate_protocol/paths.py
|
|
10
11
|
src/workstate_protocol/py.typed
|
|
11
12
|
src/workstate_protocol/skills.py
|
|
12
13
|
src/workstate_protocol.egg-info/PKG-INFO
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Tests for canonical Workstate env-var reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from workstate_protocol import resolve_env_alias
|
|
8
|
+
from workstate_protocol import env_aliases
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_canonical_name_wins():
|
|
12
|
+
env = {"WORKSTATE_STATE_DIR": "/new", "AGENT_HANDOFF_STATE_DIR": "/old"}
|
|
13
|
+
value = resolve_env_alias("WORKSTATE_STATE_DIR", env=env)
|
|
14
|
+
assert value == "/new"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_legacy_name_is_ignored():
|
|
18
|
+
env = {"AGENT_HANDOFF_STATE_DIR": "/old"}
|
|
19
|
+
assert resolve_env_alias("WORKSTATE_STATE_DIR", env=env) is None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_legacy_args_are_not_supported():
|
|
23
|
+
with pytest.raises(TypeError):
|
|
24
|
+
resolve_env_alias("WORKSTATE_STATE_DIR", "AGENT_HANDOFF_STATE_DIR", env={})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_warn_once_reset_hook_is_removed():
|
|
28
|
+
assert not hasattr(env_aliases, "reset_alias_warnings")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_blank_canonical_returns_default_not_legacy():
|
|
32
|
+
env = {"WORKSTATE_STATE_DIR": " ", "AGENT_HANDOFF_STATE_DIR": "/old"}
|
|
33
|
+
value = resolve_env_alias("WORKSTATE_STATE_DIR", env=env, default="/fallback")
|
|
34
|
+
assert value == "/fallback"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_unset_returns_default():
|
|
38
|
+
assert resolve_env_alias("WORKSTATE_STATE_DIR", env={}) is None
|
|
39
|
+
assert resolve_env_alias("WORKSTATE_STATE_DIR", env={}, default="/fallback") == "/fallback"
|
|
@@ -111,7 +111,7 @@ def test_lock_models_roundtrip() -> None:
|
|
|
111
111
|
"schema_version": 1,
|
|
112
112
|
"plugin": "workstate-system",
|
|
113
113
|
"base_remote_sha": "d" * 40,
|
|
114
|
-
"effective_root": ".
|
|
114
|
+
"effective_root": ".workstate/generated/plugins/workstate-system/effective/claude",
|
|
115
115
|
"components": [
|
|
116
116
|
{
|
|
117
117
|
"component_kind": "skill",
|
{workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/tests/test_skill_manifest_real_skills.py
RENAMED
|
@@ -28,12 +28,12 @@ def _resolve_skills_root() -> pathlib.Path | None:
|
|
|
28
28
|
"""Locate ``packages/workstate-system/skills`` robustly.
|
|
29
29
|
|
|
30
30
|
Resolution order:
|
|
31
|
-
1. ``
|
|
31
|
+
1. ``WORKSTATE_SYSTEM_SKILLS_ROOT`` env var override.
|
|
32
32
|
2. Walk up from this file looking for a sibling
|
|
33
33
|
``packages/workstate-system`` directory.
|
|
34
34
|
3. Sibling-package fallback two levels up.
|
|
35
35
|
"""
|
|
36
|
-
env_override = os.environ.get("
|
|
36
|
+
env_override = os.environ.get("WORKSTATE_SYSTEM_SKILLS_ROOT")
|
|
37
37
|
if env_override:
|
|
38
38
|
candidate = pathlib.Path(env_override).expanduser()
|
|
39
39
|
return candidate if candidate.is_dir() else None
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"""Tier-4 env-var alias resolution for the Workstate rebrand.
|
|
2
|
-
|
|
3
|
-
implementation note Slice C / B4. During the one-release cutover from the legacy
|
|
4
|
-
``AGENT_HANDOFF_*`` / ``AGENT_ORCHESTRATOR_*`` / ``AGENTIC_*`` /
|
|
5
|
-
``MCP_AGENT_HANDOFF_*`` env-var names to the canonical ``WORKSTATE_*`` prefix,
|
|
6
|
-
:func:`resolve_env_alias` reads the new name first, then falls back to any
|
|
7
|
-
legacy alias, emitting exactly one :class:`DeprecationWarning` per legacy name
|
|
8
|
-
that is actually read. The write side (exports, generated config) always sets
|
|
9
|
-
the new ``WORKSTATE_*`` name only — this module is read-side compatibility so
|
|
10
|
-
existing shells / CI that still export the old names keep working through the
|
|
11
|
-
cutover release, with a nudge to migrate.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import os
|
|
17
|
-
import warnings
|
|
18
|
-
from collections.abc import Mapping
|
|
19
|
-
from typing import overload
|
|
20
|
-
|
|
21
|
-
__all__ = ["resolve_env_alias", "reset_alias_warnings"]
|
|
22
|
-
|
|
23
|
-
# Legacy names already warned about, so the deprecation nudge fires once per
|
|
24
|
-
# process regardless of how many call sites read the same var.
|
|
25
|
-
_warned_legacy: set[str] = set()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def reset_alias_warnings() -> None:
|
|
29
|
-
"""Clear the warn-once ledger. Intended for tests."""
|
|
30
|
-
|
|
31
|
-
_warned_legacy.clear()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _non_empty(value: str | None) -> str | None:
|
|
35
|
-
if value is None:
|
|
36
|
-
return None
|
|
37
|
-
stripped = value.strip()
|
|
38
|
-
return stripped or None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@overload
|
|
42
|
-
def resolve_env_alias(
|
|
43
|
-
canonical: str,
|
|
44
|
-
*legacy: str,
|
|
45
|
-
env: Mapping[str, str] | None = ...,
|
|
46
|
-
default: str,
|
|
47
|
-
) -> str: ...
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@overload
|
|
51
|
-
def resolve_env_alias(
|
|
52
|
-
canonical: str,
|
|
53
|
-
*legacy: str,
|
|
54
|
-
env: Mapping[str, str] | None = ...,
|
|
55
|
-
default: None = ...,
|
|
56
|
-
) -> str | None: ...
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def resolve_env_alias(
|
|
60
|
-
canonical: str,
|
|
61
|
-
*legacy: str,
|
|
62
|
-
env: Mapping[str, str] | None = None,
|
|
63
|
-
default: str | None = None,
|
|
64
|
-
) -> str | None:
|
|
65
|
-
"""Resolve an env var across its canonical and legacy alias names.
|
|
66
|
-
|
|
67
|
-
Precedence: the canonical ``WORKSTATE_*`` name wins when set to a
|
|
68
|
-
non-blank value. Otherwise the first non-blank legacy alias (in the order
|
|
69
|
-
given) is returned, and a :class:`DeprecationWarning` is emitted once per
|
|
70
|
-
process for that legacy name, naming the ``canonical`` replacement. When
|
|
71
|
-
nothing is set, ``default`` is returned.
|
|
72
|
-
|
|
73
|
-
Blank/whitespace-only values are treated as unset, matching the existing
|
|
74
|
-
``_first_non_empty_env`` behaviour in the handoff package.
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
source = os.environ if env is None else env
|
|
78
|
-
|
|
79
|
-
canonical_value = _non_empty(source.get(canonical))
|
|
80
|
-
if canonical_value is not None:
|
|
81
|
-
return canonical_value
|
|
82
|
-
|
|
83
|
-
for name in legacy:
|
|
84
|
-
legacy_value = _non_empty(source.get(name))
|
|
85
|
-
if legacy_value is not None:
|
|
86
|
-
if name not in _warned_legacy:
|
|
87
|
-
_warned_legacy.add(name)
|
|
88
|
-
warnings.warn(
|
|
89
|
-
f"Environment variable {name} is deprecated; "
|
|
90
|
-
f"set {canonical} instead. The legacy name is read for "
|
|
91
|
-
f"one release and will be removed.",
|
|
92
|
-
DeprecationWarning,
|
|
93
|
-
stacklevel=2,
|
|
94
|
-
)
|
|
95
|
-
return legacy_value
|
|
96
|
-
|
|
97
|
-
return default
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"""Tests for the Tier-4 env-var alias shim.
|
|
2
|
-
|
|
3
|
-
implementation note Slice C / B4: ``workstate_protocol.env_aliases`` resolves the
|
|
4
|
-
canonical ``WORKSTATE_*`` env-var name first, then falls back to any legacy
|
|
5
|
-
alias (``AGENT_HANDOFF_*`` / ``AGENT_ORCHESTRATOR_*`` / ``AGENTIC_*`` /
|
|
6
|
-
``MCP_AGENT_HANDOFF_*``) for one release, emitting exactly one
|
|
7
|
-
``DeprecationWarning`` per legacy name that is actually read. Exports always
|
|
8
|
-
set the new name only; this shim is read-side compatibility during cutover.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import warnings
|
|
14
|
-
|
|
15
|
-
import pytest
|
|
16
|
-
|
|
17
|
-
from workstate_protocol import resolve_env_alias
|
|
18
|
-
from workstate_protocol.env_aliases import reset_alias_warnings
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@pytest.fixture(autouse=True)
|
|
22
|
-
def _clean_warn_state():
|
|
23
|
-
reset_alias_warnings()
|
|
24
|
-
yield
|
|
25
|
-
reset_alias_warnings()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_canonical_name_wins_without_warning():
|
|
29
|
-
env = {"WORKSTATE_STATE_DIR": "/new", "AGENT_HANDOFF_STATE_DIR": "/old"}
|
|
30
|
-
with warnings.catch_warnings():
|
|
31
|
-
warnings.simplefilter("error", DeprecationWarning)
|
|
32
|
-
value = resolve_env_alias(
|
|
33
|
-
"WORKSTATE_STATE_DIR", "AGENT_HANDOFF_STATE_DIR", env=env
|
|
34
|
-
)
|
|
35
|
-
assert value == "/new"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def test_legacy_fallback_emits_one_deprecation_warning():
|
|
39
|
-
env = {"AGENT_HANDOFF_STATE_DIR": "/old"}
|
|
40
|
-
with warnings.catch_warnings(record=True) as caught:
|
|
41
|
-
warnings.simplefilter("always", DeprecationWarning)
|
|
42
|
-
value = resolve_env_alias(
|
|
43
|
-
"WORKSTATE_STATE_DIR", "AGENT_HANDOFF_STATE_DIR", env=env
|
|
44
|
-
)
|
|
45
|
-
assert value == "/old"
|
|
46
|
-
dep = [w for w in caught if issubclass(w.category, DeprecationWarning)]
|
|
47
|
-
assert len(dep) == 1
|
|
48
|
-
msg = str(dep[0].message)
|
|
49
|
-
# The warning must name both the deprecated and the canonical replacement.
|
|
50
|
-
assert "AGENT_HANDOFF_STATE_DIR" in msg
|
|
51
|
-
assert "WORKSTATE_STATE_DIR" in msg
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_warn_once_per_legacy_name():
|
|
55
|
-
env = {"AGENT_HANDOFF_STATE_DIR": "/old"}
|
|
56
|
-
with warnings.catch_warnings(record=True) as caught:
|
|
57
|
-
warnings.simplefilter("always", DeprecationWarning)
|
|
58
|
-
first = resolve_env_alias(
|
|
59
|
-
"WORKSTATE_STATE_DIR", "AGENT_HANDOFF_STATE_DIR", env=env
|
|
60
|
-
)
|
|
61
|
-
second = resolve_env_alias(
|
|
62
|
-
"WORKSTATE_STATE_DIR", "AGENT_HANDOFF_STATE_DIR", env=env
|
|
63
|
-
)
|
|
64
|
-
assert first == second == "/old"
|
|
65
|
-
dep = [w for w in caught if issubclass(w.category, DeprecationWarning)]
|
|
66
|
-
assert len(dep) == 1
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_first_present_legacy_wins_among_multiple_aliases():
|
|
70
|
-
env = {"AGENTIC_LANE_ID": "lane-2"}
|
|
71
|
-
value = resolve_env_alias(
|
|
72
|
-
"WORKSTATE_LANE_ID", "AGENT_HANDOFF_LANE_ID", "AGENTIC_LANE_ID", env=env
|
|
73
|
-
)
|
|
74
|
-
assert value == "lane-2"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def test_blank_canonical_falls_back_to_legacy():
|
|
78
|
-
env = {"WORKSTATE_STATE_DIR": " ", "AGENT_HANDOFF_STATE_DIR": "/old"}
|
|
79
|
-
value = resolve_env_alias("WORKSTATE_STATE_DIR", "AGENT_HANDOFF_STATE_DIR", env=env)
|
|
80
|
-
assert value == "/old"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_unset_returns_default_without_warning():
|
|
84
|
-
with warnings.catch_warnings():
|
|
85
|
-
warnings.simplefilter("error", DeprecationWarning)
|
|
86
|
-
assert (
|
|
87
|
-
resolve_env_alias("WORKSTATE_STATE_DIR", "AGENT_HANDOFF_STATE_DIR", env={})
|
|
88
|
-
is None
|
|
89
|
-
)
|
|
90
|
-
assert (
|
|
91
|
-
resolve_env_alias(
|
|
92
|
-
"WORKSTATE_STATE_DIR",
|
|
93
|
-
"AGENT_HANDOFF_STATE_DIR",
|
|
94
|
-
env={},
|
|
95
|
-
default="/fallback",
|
|
96
|
-
)
|
|
97
|
-
== "/fallback"
|
|
98
|
-
)
|
|
File without changes
|
|
File without changes
|
{workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol/branch_naming.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/requires.txt
RENAMED
|
File without changes
|
{workstate_protocol-0.1.5 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|