workstate-protocol 0.1.6__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.6 → workstate_protocol-0.1.7}/PKG-INFO +2 -2
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/README.md +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/pyproject.toml +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/__init__.py +0 -6
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/bootstrap.py +48 -10
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/branch_naming.py +13 -13
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/handoff.py +3 -3
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/hooks.py +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/paths.py +4 -19
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/skills.py +2 -2
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/PKG-INFO +2 -2
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_bootstrap_manifest.py +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_branch_grammar_registry.py +7 -7
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_branch_naming.py +45 -45
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_handoff_schema.py +8 -8
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_skill_manifest_real_skills.py +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/setup.cfg +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/compaction.py +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/env_aliases.py +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/py.typed +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/SOURCES.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/dependency_links.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/requires.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/top_level.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_env_aliases.py +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_package_metadata.py +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_plugin_override_schema.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
|
|
@@ -17,7 +17,7 @@ Requires-Dist: pytest>=8; extra == "dev"
|
|
|
17
17
|
|
|
18
18
|
Single source of truth for cross-repo contracts in the Workstate system. Pydantic v2 is canonical; JSON Schema artifacts under `schemas/` are generated from the models so non-Python consumers (hook scripts, future TS/JS tooling) can validate without importing Python.
|
|
19
19
|
|
|
20
|
-
## Schemas (rolled out incrementally per founding
|
|
20
|
+
## Schemas (rolled out incrementally per founding implementation note)
|
|
21
21
|
|
|
22
22
|
| Status | Module | Schema |
|
|
23
23
|
| --- | --- | --- |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Single source of truth for cross-repo contracts in the Workstate system. Pydantic v2 is canonical; JSON Schema artifacts under `schemas/` are generated from the models so non-Python consumers (hook scripts, future TS/JS tooling) can validate without importing Python.
|
|
4
4
|
|
|
5
|
-
## Schemas (rolled out incrementally per founding
|
|
5
|
+
## Schemas (rolled out incrementally per founding implementation note)
|
|
6
6
|
|
|
7
7
|
| Status | Module | Schema |
|
|
8
8
|
| --- | --- | --- |
|
|
@@ -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"
|
|
@@ -37,10 +37,7 @@ from .paths import (
|
|
|
37
37
|
DOCS_MIRROR_DIR,
|
|
38
38
|
HARNESS_CONTRACT_RELPATH,
|
|
39
39
|
INSTRUCTIONS_RELPATH,
|
|
40
|
-
LEGACY_DOCS_MIRROR_DIR,
|
|
41
|
-
LEGACY_RUNTIME_ROOT_DIRNAME,
|
|
42
40
|
RULES_DIR,
|
|
43
|
-
RUNTIME_PATH_RENAMES,
|
|
44
41
|
RUNTIME_ROOT_DIRNAME,
|
|
45
42
|
docs_mirror_path,
|
|
46
43
|
runtime_root_path,
|
|
@@ -59,14 +56,11 @@ __all__ = [
|
|
|
59
56
|
"HandoffState",
|
|
60
57
|
"HandoffStatus",
|
|
61
58
|
"INSTRUCTIONS_RELPATH",
|
|
62
|
-
"LEGACY_DOCS_MIRROR_DIR",
|
|
63
|
-
"LEGACY_RUNTIME_ROOT_DIRNAME",
|
|
64
59
|
"OverlayConfigEntry",
|
|
65
60
|
"OverlaySurface",
|
|
66
61
|
"PostToolUseEvent",
|
|
67
62
|
"PreToolUseEvent",
|
|
68
63
|
"RULES_DIR",
|
|
69
|
-
"RUNTIME_PATH_RENAMES",
|
|
70
64
|
"RUNTIME_ROOT_DIRNAME",
|
|
71
65
|
"SessionStartEvent",
|
|
72
66
|
"SkillManifest",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Bootstrap install manifest schema (Schema #5 from founding
|
|
1
|
+
"""Bootstrap install manifest schema (Schema #5 from founding implementation note).
|
|
2
2
|
|
|
3
3
|
The wire shape ``workstate-bootstrap`` writes to
|
|
4
4
|
``<target>/.workstate-overlay.json``. Captures the contract between
|
|
@@ -27,14 +27,16 @@ 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), "
|
|
34
36
|
"'local' (target-owned), 'overlapping' (target overrides shared), "
|
|
35
37
|
"'generated' (per-agent surface produced by generate_agent_workflows.py "
|
|
36
|
-
"during install —
|
|
37
|
-
"'lifecycle' (
|
|
38
|
+
"during install — implementation note step 1), "
|
|
39
|
+
"'lifecycle' (implementation note hoisted Make fragment + runner package "
|
|
38
40
|
"— copied, not symlinked, so the consumer can run `make context` "
|
|
39
41
|
"without the workstate-system packaging tree)."
|
|
40
42
|
),
|
|
@@ -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)
|
|
@@ -261,7 +279,27 @@ class BootstrapManifest(BaseModel):
|
|
|
261
279
|
default=None,
|
|
262
280
|
description=(
|
|
263
281
|
"Optional explicit plugin override root recorded by bootstrap so "
|
|
264
|
-
"later doctor/update/repair runs can reuse a non-default
|
|
282
|
+
"later doctor/update/repair runs can reuse a non-default WORKSTATE-REF-03 "
|
|
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
|
{workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol/branch_naming.py
RENAMED
|
@@ -4,13 +4,13 @@ This module is the SOLE owner of ``TASK_REF_RE``,
|
|
|
4
4
|
``derive_task_ref_candidates`` and ``format_suggested_branch_name``.
|
|
5
5
|
Every gate (post-checkout warn, PreToolUse block, pre-commit hard gate,
|
|
6
6
|
pre-push mirror) imports from here. ``workstate_handoff_mcp`` re-exports
|
|
7
|
-
the same objects without redefinition. See
|
|
7
|
+
the same objects without redefinition. See implementation note for context.
|
|
8
8
|
|
|
9
9
|
Case convention
|
|
10
10
|
---------------
|
|
11
11
|
|
|
12
|
-
Branches are lowercase (``feature/
|
|
13
|
-
Workstate handoff task table are uppercase (``
|
|
12
|
+
Branches are lowercase (``feature/WORKSTATE-37-foo``). Task refs in the
|
|
13
|
+
Workstate handoff task table are uppercase (``WORKSTATE-REF-37``).
|
|
14
14
|
``derive_task_ref_candidates`` returns lowercase candidates; callers
|
|
15
15
|
``.upper()`` each candidate before intersecting against the live task
|
|
16
16
|
table. ``format_suggested_branch_name`` accepts task refs in either
|
|
@@ -26,7 +26,7 @@ from dataclasses import dataclass, field
|
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
28
|
"BRANCH_GRAMMAR_REGISTRY",
|
|
29
|
-
"
|
|
29
|
+
"BranWORKSTATElassification",
|
|
30
30
|
"BranchGrammarEntry",
|
|
31
31
|
"TASK_REF_RE",
|
|
32
32
|
"__protocol_version__",
|
|
@@ -51,7 +51,7 @@ TASK_REF_RE: re.Pattern[str] = re.compile(
|
|
|
51
51
|
# >=2 hyphen-separated lowercase / digit segments; each segment is
|
|
52
52
|
# non-empty. The alternation enforces a `<prefix>-<rest>` shape so
|
|
53
53
|
# single-word branches like ``feature/foo`` are rejected and
|
|
54
|
-
# conventional task refs (``feature/
|
|
54
|
+
# conventional task refs (``feature/WORKSTATE-37``,
|
|
55
55
|
# ``feature/maint-dirty-br-01``) still match.
|
|
56
56
|
r"(?P<task_ref>[a-z0-9]+(?:-[a-z0-9]+)+)"
|
|
57
57
|
r"$"
|
|
@@ -69,8 +69,8 @@ def derive_task_ref_candidates(branch_name: str) -> list[str]:
|
|
|
69
69
|
|
|
70
70
|
Examples
|
|
71
71
|
--------
|
|
72
|
-
>>> derive_task_ref_candidates("feature/
|
|
73
|
-
['
|
|
72
|
+
>>> derive_task_ref_candidates("feature/WORKSTATE-37-branch-naming-enforcement")
|
|
73
|
+
['WORKSTATE-37-branch-naming-enforcement', 'WORKSTATE-37']
|
|
74
74
|
>>> derive_task_ref_candidates("feature/maint-dirty-br-01")
|
|
75
75
|
['maint-dirty-br-01', 'maint-dirty-br']
|
|
76
76
|
>>> derive_task_ref_candidates("fix/foo")
|
|
@@ -107,7 +107,7 @@ def select_task_ref_candidate(
|
|
|
107
107
|
when both the base and the follow-up are registered. When the
|
|
108
108
|
registry is non-empty but no candidate intersects, returns
|
|
109
109
|
``None`` — the strict invariant is that we never name a candidate
|
|
110
|
-
that is absent from a populated registry (
|
|
110
|
+
that is absent from a populated registry (WORKSTATE65-BR-02).
|
|
111
111
|
- If ``known_task_refs`` is empty or ``None`` (no registry context
|
|
112
112
|
available at all), falls back to the shortest digit-bearing
|
|
113
113
|
prefix. This is the no-context degradation path that keeps
|
|
@@ -152,7 +152,7 @@ def _has_digit(value: str) -> bool:
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
# ---------------------------------------------------------------------------
|
|
155
|
-
# Branch grammar registry (
|
|
155
|
+
# Branch grammar registry (implementation note implementation note)
|
|
156
156
|
# ---------------------------------------------------------------------------
|
|
157
157
|
#
|
|
158
158
|
# Canonical pattern (``feature/<task-ref>``) plus each documented
|
|
@@ -170,7 +170,7 @@ class BranchGrammarEntry:
|
|
|
170
170
|
|
|
171
171
|
|
|
172
172
|
@dataclass(frozen=True)
|
|
173
|
-
class
|
|
173
|
+
class BranWORKSTATElassification:
|
|
174
174
|
kind: str
|
|
175
175
|
branch: str
|
|
176
176
|
|
|
@@ -193,8 +193,8 @@ BRANCH_GRAMMAR_REGISTRY: tuple[BranchGrammarEntry, ...] = (
|
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
def classify_branch(name: str) ->
|
|
197
|
-
"""Return the :class:`
|
|
196
|
+
def classify_branch(name: str) -> BranWORKSTATElassification | None:
|
|
197
|
+
"""Return the :class:`BranWORKSTATElassification` for ``name`` or ``None``.
|
|
198
198
|
|
|
199
199
|
Unknown patterns return ``None`` (fail-closed); each consumer
|
|
200
200
|
decides whether unknown means warn or block.
|
|
@@ -202,7 +202,7 @@ def classify_branch(name: str) -> BranchClassification | None:
|
|
|
202
202
|
|
|
203
203
|
for entry in BRANCH_GRAMMAR_REGISTRY:
|
|
204
204
|
if entry.regex.match(name):
|
|
205
|
-
return
|
|
205
|
+
return BranWORKSTATElassification(kind=entry.kind, branch=name)
|
|
206
206
|
return None
|
|
207
207
|
|
|
208
208
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Handoff state schemas (Schema #1 from founding
|
|
1
|
+
"""Handoff state schemas (Schema #1 from founding implementation note).
|
|
2
2
|
|
|
3
3
|
These are the cross-repo contract types for handoff state. They model
|
|
4
4
|
the wire shape that ``mcp-workstate-handoff`` exposes to MCP clients and
|
|
@@ -23,13 +23,13 @@ from pydantic import BaseModel, ConfigDict, Field, StringConstraints
|
|
|
23
23
|
# Primitive identifiers and enums
|
|
24
24
|
# ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
# A task_ref is a short identifier like "
|
|
26
|
+
# A task_ref is a short identifier like "WORKSTATE-REF-17-14" or "WORKSTATE-REF-7" or "AAA-1".
|
|
27
27
|
# We require non-empty string with a permissive character class so domain
|
|
28
28
|
# tools can introduce new prefixes without re-releasing the protocol.
|
|
29
29
|
TaskRef = Annotated[
|
|
30
30
|
str,
|
|
31
31
|
StringConstraints(strip_whitespace=True, min_length=1, max_length=128),
|
|
32
|
-
Field(description="Short task identifier, e.g. '
|
|
32
|
+
Field(description="Short task identifier, e.g. 'WORKSTATE-REF-17-14' or 'WORKSTATE-REF-7'."),
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Hook event payload schemas (Schema #3 from founding
|
|
1
|
+
"""Hook event payload schemas (Schema #3 from founding implementation note).
|
|
2
2
|
|
|
3
3
|
Models the JSON Claude Code delivers to hook scripts on stdin. Each
|
|
4
4
|
event type is modeled separately rather than as a discriminated union
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
"""Canonical Workstate runtime + doc-mirror path roots — single source of truth.
|
|
2
2
|
|
|
3
|
-
The runtime install directory and the mirrored docs/contracts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Every package resolves these through this module so the names live in exactly
|
|
8
|
-
one place: a future path change is a one-line flip here, not a repo-wide sweep.
|
|
9
|
-
The ``LEGACY_*`` names and :data:`RUNTIME_PATH_RENAMES` exist so bootstrap can
|
|
10
|
-
detect and migrate an old checkout forward for one release.
|
|
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.
|
|
11
7
|
"""
|
|
12
8
|
|
|
13
9
|
from __future__ import annotations
|
|
@@ -17,18 +13,10 @@ from pathlib import Path
|
|
|
17
13
|
# Runtime install root — bootstrap materializes overlay surfaces and the remote
|
|
18
14
|
# clone under ``<target>/.workstate/``.
|
|
19
15
|
RUNTIME_ROOT_DIRNAME = ".workstate"
|
|
20
|
-
LEGACY_RUNTIME_ROOT_DIRNAME = ".agentic"
|
|
21
16
|
|
|
22
17
|
# Mirrored docs / contracts path — the SHARED_SURFACES consumed at install time
|
|
23
18
|
# (rules, contracts, templates) live under ``docs/workstate/``.
|
|
24
19
|
DOCS_MIRROR_DIR = "docs/workstate"
|
|
25
|
-
LEGACY_DOCS_MIRROR_DIR = "docs/agentic"
|
|
26
|
-
|
|
27
|
-
# Ordered (legacy -> canonical) pairs for migration / detection sweeps.
|
|
28
|
-
RUNTIME_PATH_RENAMES: tuple[tuple[str, str], ...] = (
|
|
29
|
-
(LEGACY_RUNTIME_ROOT_DIRNAME, RUNTIME_ROOT_DIRNAME),
|
|
30
|
-
(LEGACY_DOCS_MIRROR_DIR, DOCS_MIRROR_DIR),
|
|
31
|
-
)
|
|
32
20
|
|
|
33
21
|
# Common derived locations under the canonical docs mirror.
|
|
34
22
|
CONTRACTS_DIR = f"{DOCS_MIRROR_DIR}/contracts"
|
|
@@ -41,10 +29,7 @@ __all__ = [
|
|
|
41
29
|
"DOCS_MIRROR_DIR",
|
|
42
30
|
"HARNESS_CONTRACT_RELPATH",
|
|
43
31
|
"INSTRUCTIONS_RELPATH",
|
|
44
|
-
"LEGACY_DOCS_MIRROR_DIR",
|
|
45
|
-
"LEGACY_RUNTIME_ROOT_DIRNAME",
|
|
46
32
|
"RULES_DIR",
|
|
47
|
-
"RUNTIME_PATH_RENAMES",
|
|
48
33
|
"RUNTIME_ROOT_DIRNAME",
|
|
49
34
|
"docs_mirror_path",
|
|
50
35
|
"runtime_root_path",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Skill manifest schema (Schema #4 from founding
|
|
1
|
+
"""Skill manifest schema (Schema #4 from founding implementation note).
|
|
2
2
|
|
|
3
3
|
Structured contract for the canonical neutral skill layout
|
|
4
|
-
``skills/<slug>/skill.yaml`` (
|
|
4
|
+
``skills/<slug>/skill.yaml`` (implementation note step 1). The pre-Plan-0002
|
|
5
5
|
``.claude/skills/<slug>/SKILL.md`` frontmatter is now a *generated*
|
|
6
6
|
artifact in target repos and is no longer a source-of-truth surface.
|
|
7
7
|
Enforces the Tier 1 / Tier 2 / Tier 3 portability boundary by
|
{workstate_protocol-0.1.6 → 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
|
|
@@ -17,7 +17,7 @@ Requires-Dist: pytest>=8; extra == "dev"
|
|
|
17
17
|
|
|
18
18
|
Single source of truth for cross-repo contracts in the Workstate system. Pydantic v2 is canonical; JSON Schema artifacts under `schemas/` are generated from the models so non-Python consumers (hook scripts, future TS/JS tooling) can validate without importing Python.
|
|
19
19
|
|
|
20
|
-
## Schemas (rolled out incrementally per founding
|
|
20
|
+
## Schemas (rolled out incrementally per founding implementation note)
|
|
21
21
|
|
|
22
22
|
| Status | Module | Schema |
|
|
23
23
|
| --- | --- | --- |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Schema tests for BootstrapManifest mcp_servers field.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WORKSTATE-REF-50 implementation note adds a ``mcp_servers: list[str]`` field to the manifest
|
|
4
4
|
so the bootstrap ledger can carry the previously-managed server names.
|
|
5
5
|
``sync_mcp_configs(prune_removed_managed=True)`` reads this list to
|
|
6
6
|
distinguish managed launchers (subject to prune) from third-party ones
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""implementation note implementation note — branch-grammar registry."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -16,11 +16,11 @@ def test_registry_includes_documented_exception_kinds() -> None:
|
|
|
16
16
|
@pytest.mark.parametrize(
|
|
17
17
|
"branch,expected_kind",
|
|
18
18
|
[
|
|
19
|
-
("feature/
|
|
19
|
+
("feature/WORKSTATE-37-branch-naming-enforcement", "feature"),
|
|
20
20
|
("release/0.9.1", "release"),
|
|
21
|
-
("hotfix/
|
|
21
|
+
("hotfix/WORKSTATE-99-fix-bug", "hotfix"),
|
|
22
22
|
("maint/cleanup-stale-rows", "maint"),
|
|
23
|
-
("revert/
|
|
23
|
+
("revert/WORKSTATE-12-bad-merge", "revert"),
|
|
24
24
|
],
|
|
25
25
|
)
|
|
26
26
|
def test_classify_branch_returns_correct_kind(branch: str, expected_kind: str) -> None:
|
|
@@ -34,11 +34,11 @@ def test_classify_branch_returns_correct_kind(branch: str, expected_kind: str) -
|
|
|
34
34
|
@pytest.mark.parametrize(
|
|
35
35
|
"branch",
|
|
36
36
|
[
|
|
37
|
-
"feature/
|
|
37
|
+
"feature/WORKSTATE-37-branch-naming-enforcement",
|
|
38
38
|
"release/0.9.1",
|
|
39
|
-
"hotfix/
|
|
39
|
+
"hotfix/WORKSTATE-99-fix-bug",
|
|
40
40
|
"maint/cleanup-stale-rows",
|
|
41
|
-
"revert/
|
|
41
|
+
"revert/WORKSTATE-12-bad-merge",
|
|
42
42
|
],
|
|
43
43
|
)
|
|
44
44
|
def test_is_allowed_branch_passes_for_documented_patterns(branch: str) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tests for the canonical branch-naming rule.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
implementation note implementation note: ``workstate_protocol.branch_naming`` exposes
|
|
4
4
|
``TASK_REF_RE``, ``derive_task_ref_candidates``,
|
|
5
5
|
``format_suggested_branch_name``, and ``__protocol_version__`` as the
|
|
6
6
|
single source of truth for branch-naming validation across every gate.
|
|
@@ -23,8 +23,8 @@ from workstate_protocol.branch_naming import (
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
POSITIVE_CORPUS = (
|
|
26
|
-
"feature/
|
|
27
|
-
"feature/
|
|
26
|
+
"feature/WORKSTATE-35",
|
|
27
|
+
"feature/WORKSTATE-37-branch-naming-enforcement",
|
|
28
28
|
"feature/maint-dirty-br-01",
|
|
29
29
|
"feature/maint-archive-stale-20260502",
|
|
30
30
|
"feature/plan-0006",
|
|
@@ -43,11 +43,11 @@ NEGATIVE_CORPUS = (
|
|
|
43
43
|
"feature/no-digits-here",
|
|
44
44
|
"hotfix/x",
|
|
45
45
|
"release/v1",
|
|
46
|
-
"feature/
|
|
46
|
+
"feature/WORKSTATE-REF-37", # uppercase
|
|
47
47
|
"feature/-x", # leading hyphen
|
|
48
48
|
"feature/123-foo", # leading digit segment
|
|
49
49
|
"feature/", # empty task ref
|
|
50
|
-
"FEATURE/
|
|
50
|
+
"FEATURE/WORKSTATE-37", # uppercase prefix
|
|
51
51
|
"",
|
|
52
52
|
)
|
|
53
53
|
|
|
@@ -74,15 +74,15 @@ def test_protocol_version_marker_is_string() -> None:
|
|
|
74
74
|
@pytest.mark.parametrize(
|
|
75
75
|
"branch,expected",
|
|
76
76
|
[
|
|
77
|
-
# Walk every digit-bearing prefix from longest to shortest. ``
|
|
77
|
+
# Walk every digit-bearing prefix from longest to shortest. ``WORKSTATE`` is
|
|
78
78
|
# dropped because it has no digit.
|
|
79
|
-
("feature/
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
79
|
+
("feature/WORKSTATE-37-branch-naming-enforcement", [
|
|
80
|
+
"WORKSTATE-37-branch-naming-enforcement",
|
|
81
|
+
"WORKSTATE-37-branch-naming",
|
|
82
|
+
"WORKSTATE-37-branch",
|
|
83
|
+
"WORKSTATE-37",
|
|
84
84
|
]),
|
|
85
|
-
("feature/
|
|
85
|
+
("feature/WORKSTATE-37", ["WORKSTATE-37"]),
|
|
86
86
|
# Only the full ref carries a digit; shorter prefixes are digit-less.
|
|
87
87
|
("feature/maint-dirty-br-01", ["maint-dirty-br-01"]),
|
|
88
88
|
("feature/plan-0006", ["plan-0006"]),
|
|
@@ -105,7 +105,7 @@ def test_derive_returns_lowercase_for_conforming() -> None:
|
|
|
105
105
|
The derivation MUST stay lowercase so that intersection logic owns the
|
|
106
106
|
case conversion rather than guessing it here."""
|
|
107
107
|
candidates = derive_task_ref_candidates(
|
|
108
|
-
"feature/
|
|
108
|
+
"feature/WORKSTATE-37-branch-naming-enforcement"
|
|
109
109
|
)
|
|
110
110
|
assert all(c == c.lower() for c in candidates)
|
|
111
111
|
|
|
@@ -113,11 +113,11 @@ def test_derive_returns_lowercase_for_conforming() -> None:
|
|
|
113
113
|
@pytest.mark.parametrize(
|
|
114
114
|
"task_ref,slug,expected",
|
|
115
115
|
[
|
|
116
|
-
("
|
|
117
|
-
("
|
|
118
|
-
("
|
|
119
|
-
"feature/
|
|
120
|
-
("
|
|
116
|
+
("WORKSTATE-REF-37", None, "feature/WORKSTATE-37"),
|
|
117
|
+
("WORKSTATE-37", None, "feature/WORKSTATE-37"),
|
|
118
|
+
("WORKSTATE-REF-37", "branch-naming-enforcement",
|
|
119
|
+
"feature/WORKSTATE-37-branch-naming-enforcement"),
|
|
120
|
+
("WORKSTATE-REF-DIRTY-BR-01", None, "feature/maint-dirty-br-01"),
|
|
121
121
|
("PLAN-0006", "rollout", "feature/plan-0006-rollout"),
|
|
122
122
|
],
|
|
123
123
|
)
|
|
@@ -145,61 +145,61 @@ def test_select_task_ref_candidate_no_registry_returns_shortest_prefix() -> None
|
|
|
145
145
|
"""
|
|
146
146
|
assert (
|
|
147
147
|
select_task_ref_candidate(
|
|
148
|
-
"feature/
|
|
148
|
+
"feature/WORKSTATE-63-fu-tighten-compaction-defaults", known_task_refs=None
|
|
149
149
|
)
|
|
150
|
-
== "
|
|
150
|
+
== "WORKSTATE-REF-63"
|
|
151
151
|
)
|
|
152
152
|
assert (
|
|
153
153
|
select_task_ref_candidate(
|
|
154
|
-
"feature/
|
|
154
|
+
"feature/WORKSTATE-63-fu-tighten-compaction-defaults", known_task_refs=()
|
|
155
155
|
)
|
|
156
|
-
== "
|
|
156
|
+
== "WORKSTATE-REF-63"
|
|
157
157
|
)
|
|
158
158
|
assert (
|
|
159
159
|
select_task_ref_candidate(
|
|
160
|
-
"feature/
|
|
160
|
+
"feature/WORKSTATE-37-branch-naming-enforcement", known_task_refs=set()
|
|
161
161
|
)
|
|
162
|
-
== "
|
|
162
|
+
== "WORKSTATE-REF-37"
|
|
163
163
|
)
|
|
164
164
|
assert (
|
|
165
165
|
select_task_ref_candidate("feature/maint-dirty-br-01", known_task_refs=None)
|
|
166
|
-
== "
|
|
166
|
+
== "WORKSTATE-REF-DIRTY-BR-01"
|
|
167
167
|
)
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
def test_select_task_ref_candidate_longest_registered_wins() -> None:
|
|
171
171
|
"""With both the base and a follow-up registered, the longer registered
|
|
172
|
-
candidate wins. This is the
|
|
172
|
+
candidate wins. This is the WORKSTATE-REF-63 vs WORKSTATE-REF-63-FU-... case that
|
|
173
173
|
motivated the selector.
|
|
174
174
|
"""
|
|
175
|
-
branch = "feature/
|
|
176
|
-
known = {"
|
|
175
|
+
branch = "feature/WORKSTATE-63-fu-tighten-compaction-defaults"
|
|
176
|
+
known = {"WORKSTATE-REF-63", "WORKSTATE-REF-63-FU-TIGHTEN-COMTASKCTION-DEFAULTS"}
|
|
177
177
|
assert (
|
|
178
178
|
select_task_ref_candidate(branch, known_task_refs=known)
|
|
179
|
-
== "
|
|
179
|
+
== "WORKSTATE-REF-63-FU-TIGHTEN-COMTASKCTION-DEFAULTS"
|
|
180
180
|
)
|
|
181
181
|
|
|
182
182
|
|
|
183
183
|
def test_select_task_ref_candidate_only_base_registered_picks_base() -> None:
|
|
184
184
|
"""When only the base ref is registered, no longer candidate intersects
|
|
185
185
|
and the base wins (it is itself a registered candidate)."""
|
|
186
|
-
branch = "feature/
|
|
186
|
+
branch = "feature/WORKSTATE-63-fu-tighten-compaction-defaults"
|
|
187
187
|
assert (
|
|
188
|
-
select_task_ref_candidate(branch, known_task_refs={"
|
|
189
|
-
== "
|
|
188
|
+
select_task_ref_candidate(branch, known_task_refs={"WORKSTATE-REF-63"})
|
|
189
|
+
== "WORKSTATE-REF-63"
|
|
190
190
|
)
|
|
191
191
|
|
|
192
192
|
|
|
193
193
|
def test_select_task_ref_candidate_no_intersection_returns_none() -> None:
|
|
194
194
|
"""If ``known_task_refs`` is non-empty but no candidate intersects, the
|
|
195
195
|
selector returns ``None`` rather than naming a candidate absent from a
|
|
196
|
-
populated registry (
|
|
196
|
+
populated registry (WORKSTATE65-BR-02 invariant: "no resolver path should
|
|
197
197
|
return a candidate absent from a non-empty registry"). The no-context
|
|
198
198
|
shortest-prefix fallback applies only when the registry is genuinely
|
|
199
199
|
empty / unavailable, not when it answered with unrelated rows."""
|
|
200
|
-
branch = "feature/
|
|
200
|
+
branch = "feature/WORKSTATE-63-fu-tighten-compaction-defaults"
|
|
201
201
|
assert (
|
|
202
|
-
select_task_ref_candidate(branch, known_task_refs={"
|
|
202
|
+
select_task_ref_candidate(branch, known_task_refs={"WORKSTATE-REF-02", "WORKSTATE-REF-99"})
|
|
203
203
|
is None
|
|
204
204
|
)
|
|
205
205
|
|
|
@@ -208,14 +208,14 @@ def test_select_task_ref_candidate_case_insensitive_intersection() -> None:
|
|
|
208
208
|
"""``derive_task_ref_candidates`` returns lowercase; registry refs are
|
|
209
209
|
canonically uppercase. The selector normalizes both sides so a
|
|
210
210
|
lowercase or mixed-case registry still resolves correctly."""
|
|
211
|
-
branch = "feature/
|
|
211
|
+
branch = "feature/WORKSTATE-63-fu-example"
|
|
212
212
|
assert (
|
|
213
|
-
select_task_ref_candidate(branch, known_task_refs={"
|
|
214
|
-
== "
|
|
213
|
+
select_task_ref_candidate(branch, known_task_refs={"WORKSTATE-63-fu-example"})
|
|
214
|
+
== "WORKSTATE-REF-63-FU-EXAMPLE"
|
|
215
215
|
)
|
|
216
216
|
assert (
|
|
217
|
-
select_task_ref_candidate(branch, known_task_refs={"
|
|
218
|
-
== "
|
|
217
|
+
select_task_ref_candidate(branch, known_task_refs={"WORKSTATE-63-Fu-Example"})
|
|
218
|
+
== "WORKSTATE-REF-63-FU-EXAMPLE"
|
|
219
219
|
)
|
|
220
220
|
|
|
221
221
|
|
|
@@ -227,22 +227,22 @@ def test_select_task_ref_candidate_non_conforming_branch_returns_none() -> None:
|
|
|
227
227
|
assert select_task_ref_candidate("main") is None
|
|
228
228
|
assert select_task_ref_candidate("") is None
|
|
229
229
|
assert (
|
|
230
|
-
select_task_ref_candidate("fix/foo", known_task_refs={"
|
|
230
|
+
select_task_ref_candidate("fix/foo", known_task_refs={"WORKSTATE-REF-1"}) is None
|
|
231
231
|
)
|
|
232
232
|
|
|
233
233
|
|
|
234
234
|
def test_select_task_ref_candidate_single_segment_unchanged() -> None:
|
|
235
|
-
"""Single-segment refs like ``
|
|
235
|
+
"""Single-segment refs like ``WORKSTATE-REF-DIRTY-BR-01`` (one task ref, no
|
|
236
236
|
follow-up suffix possible) resolve identically with or without
|
|
237
237
|
registry context."""
|
|
238
238
|
branch = "feature/maint-dirty-br-01"
|
|
239
239
|
assert (
|
|
240
|
-
select_task_ref_candidate(branch, known_task_refs={"
|
|
241
|
-
== "
|
|
240
|
+
select_task_ref_candidate(branch, known_task_refs={"WORKSTATE-REF-DIRTY-BR-01"})
|
|
241
|
+
== "WORKSTATE-REF-DIRTY-BR-01"
|
|
242
242
|
)
|
|
243
243
|
assert (
|
|
244
244
|
select_task_ref_candidate(branch, known_task_refs=None)
|
|
245
|
-
== "
|
|
245
|
+
== "WORKSTATE-REF-DIRTY-BR-01"
|
|
246
246
|
)
|
|
247
247
|
|
|
248
248
|
|
|
@@ -16,18 +16,18 @@ from workstate_protocol import (
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def test_active_task_minimal_roundtrip() -> None:
|
|
19
|
-
a = ActiveTask(task_ref="
|
|
19
|
+
a = ActiveTask(task_ref="WORKSTATE-REF-17-14", objective="probe")
|
|
20
20
|
assert a.status is HandoffStatus.in_progress
|
|
21
21
|
assert a.task_plan_path is None
|
|
22
22
|
assert a.task_plan() is None
|
|
23
23
|
dumped = a.model_dump()
|
|
24
|
-
assert dumped["task_ref"] == "
|
|
24
|
+
assert dumped["task_ref"] == "WORKSTATE-REF-17-14"
|
|
25
25
|
assert dumped["status"] == "in_progress"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def test_active_task_with_plan_metadata() -> None:
|
|
29
29
|
a = ActiveTask(
|
|
30
|
-
task_ref="
|
|
30
|
+
task_ref="WORKSTATE-REF-17-14",
|
|
31
31
|
objective="probe",
|
|
32
32
|
target_branch="feature/e17-14",
|
|
33
33
|
target_worktree_path="/tmp/worktree",
|
|
@@ -48,7 +48,7 @@ def test_active_task_allows_extra_fields_for_passthrough() -> None:
|
|
|
48
48
|
# The handoff DB row has columns we don't model yet; they must
|
|
49
49
|
# round-trip rather than blow up.
|
|
50
50
|
raw = {
|
|
51
|
-
"task_ref": "
|
|
51
|
+
"task_ref": "WORKSTATE-REF-17-14",
|
|
52
52
|
"objective": "probe",
|
|
53
53
|
"revision": 3,
|
|
54
54
|
"updated_at": "2026-04-25T12:00:00Z",
|
|
@@ -135,12 +135,12 @@ def test_handoff_status_is_constrained() -> None:
|
|
|
135
135
|
|
|
136
136
|
def test_turn_range_and_structured_summary_roundtrip() -> None:
|
|
137
137
|
summary = StructuredSummary(
|
|
138
|
-
compaction_id="C-
|
|
138
|
+
compaction_id="C-WORKSTATE-REF-34-0001",
|
|
139
139
|
session_id="session-123",
|
|
140
140
|
harness="codex",
|
|
141
|
-
task_ref="
|
|
141
|
+
task_ref="WORKSTATE-REF-34",
|
|
142
142
|
turn_range=TurnRange(start_turn=1, end_turn=42),
|
|
143
|
-
decisions=[{"decision_id": "
|
|
143
|
+
decisions=[{"decision_id": "scope_intake_WORKSTATE-34_trigger_choice", "slug": "trigger-choice"}],
|
|
144
144
|
findings_fixed=["F-1"],
|
|
145
145
|
findings_opened=["F-2"],
|
|
146
146
|
tests_verified=["pytest tests/test_schema_migrations.py -q"],
|
|
@@ -154,7 +154,7 @@ def test_turn_range_and_structured_summary_roundtrip() -> None:
|
|
|
154
154
|
assert dumped["harness"] == "codex"
|
|
155
155
|
restored = StructuredSummary.model_validate(dumped)
|
|
156
156
|
assert restored.turn_range.end_turn == 42
|
|
157
|
-
assert restored.decisions[0].decision_id == "
|
|
157
|
+
assert restored.decisions[0].decision_id == "scope_intake_WORKSTATE-34_trigger_choice"
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
def test_compaction_summary_schema_artifact_exists() -> None:
|
{workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/tests/test_skill_manifest_real_skills.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Loads real harness skills from workstate-system to confirm the
|
|
2
2
|
SkillManifest contract matches what is actually shipped.
|
|
3
3
|
|
|
4
|
-
After
|
|
4
|
+
After implementation note step 1, the canonical layout is
|
|
5
5
|
``packages/workstate-system/skills/<slug>/{skill.yaml, body.md}``.
|
|
6
6
|
The Claude-namespaced ``.claude/skills/<slug>/SKILL.md`` is a generated
|
|
7
7
|
artifact in target repos, not source. This test reads the neutral
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{workstate_protocol-0.1.6 → workstate_protocol-0.1.7}/src/workstate_protocol.egg-info/requires.txt
RENAMED
|
File without changes
|
{workstate_protocol-0.1.6 → 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
|