workstate-protocol 0.1.6__tar.gz → 0.2.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.
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/PKG-INFO +2 -2
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/README.md +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/pyproject.toml +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/__init__.py +1 -7
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/bootstrap.py +129 -20
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/branch_naming.py +13 -13
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/handoff.py +3 -3
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/hooks.py +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/paths.py +4 -19
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/skills.py +2 -2
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/PKG-INFO +2 -2
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_bootstrap_manifest.py +1 -1
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_branch_grammar_registry.py +7 -7
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_branch_naming.py +45 -45
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_handoff_schema.py +8 -8
- workstate_protocol-0.2.1/tests/test_plugin_override_schema.py +330 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_skill_manifest_real_skills.py +1 -1
- workstate_protocol-0.1.6/tests/test_plugin_override_schema.py +0 -145
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/setup.cfg +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/compaction.py +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/env_aliases.py +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/py.typed +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/SOURCES.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/dependency_links.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/requires.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/top_level.txt +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_env_aliases.py +0 -0
- {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/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.2.1
|
|
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.2.1"
|
|
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,17 +37,14 @@ 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,
|
|
47
44
|
)
|
|
48
45
|
from .skills import SkillManifest, SkillScope
|
|
49
46
|
|
|
50
|
-
__version__ = "0.1
|
|
47
|
+
__version__ = "0.2.1"
|
|
51
48
|
|
|
52
49
|
__all__ = [
|
|
53
50
|
"ActiveTask",
|
|
@@ -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
|
|
@@ -9,9 +9,18 @@ single typed shape rather than per-key heuristics.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
from pathlib import PurePosixPath
|
|
12
13
|
from typing import Annotated, Literal
|
|
13
14
|
|
|
14
|
-
from pydantic import
|
|
15
|
+
from pydantic import (
|
|
16
|
+
BaseModel,
|
|
17
|
+
ConfigDict,
|
|
18
|
+
Field,
|
|
19
|
+
StringConstraints,
|
|
20
|
+
WithJsonSchema,
|
|
21
|
+
field_validator,
|
|
22
|
+
model_validator,
|
|
23
|
+
)
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
Sha40 = Annotated[str, StringConstraints(pattern=r"^[0-9a-f]{40}$")]
|
|
@@ -20,6 +29,35 @@ PluginComponentName = Annotated[
|
|
|
20
29
|
str,
|
|
21
30
|
StringConstraints(pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]*$"),
|
|
22
31
|
]
|
|
32
|
+
PluginOverrideRelativePath = Annotated[
|
|
33
|
+
str,
|
|
34
|
+
StringConstraints(min_length=1),
|
|
35
|
+
WithJsonSchema(
|
|
36
|
+
{
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"not": {
|
|
40
|
+
"anyOf": [
|
|
41
|
+
{"pattern": "^/"},
|
|
42
|
+
{"pattern": "(^|/)\\.\\.(/|$)"},
|
|
43
|
+
{"pattern": "^\\.?$"},
|
|
44
|
+
{"pattern": "\\\\"},
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _normalize_plugin_override_path(value: str) -> str:
|
|
53
|
+
if "\\" in value:
|
|
54
|
+
raise ValueError("override paths must use POSIX '/' separators")
|
|
55
|
+
path = PurePosixPath(value)
|
|
56
|
+
if path.is_absolute() or value == "." or ".." in path.parts:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"override paths must be relative and stay under the override root"
|
|
59
|
+
)
|
|
60
|
+
return path.as_posix()
|
|
23
61
|
|
|
24
62
|
|
|
25
63
|
class OverlaySurface(BaseModel):
|
|
@@ -27,14 +65,16 @@ class OverlaySurface(BaseModel):
|
|
|
27
65
|
|
|
28
66
|
model_config = ConfigDict(extra="allow")
|
|
29
67
|
|
|
30
|
-
path: str = Field(
|
|
68
|
+
path: str = Field(
|
|
69
|
+
description="Repo-relative surface path (e.g. '.claude/skills/handoff-lifecycle')."
|
|
70
|
+
)
|
|
31
71
|
source: Literal["shared", "local", "overlapping", "generated", "lifecycle"] = Field(
|
|
32
72
|
description=(
|
|
33
73
|
"Origin tier: 'shared' (symlinked from workstate-system), "
|
|
34
74
|
"'local' (target-owned), 'overlapping' (target overrides shared), "
|
|
35
75
|
"'generated' (per-agent surface produced by generate_agent_workflows.py "
|
|
36
|
-
"during install —
|
|
37
|
-
"'lifecycle' (
|
|
76
|
+
"during install — implementation note step 1), "
|
|
77
|
+
"'lifecycle' (implementation note hoisted Make fragment + runner package "
|
|
38
78
|
"— copied, not symlinked, so the consumer can run `make context` "
|
|
39
79
|
"without the workstate-system packaging tree)."
|
|
40
80
|
),
|
|
@@ -64,17 +104,31 @@ class PluginSkillOverride(BaseModel):
|
|
|
64
104
|
|
|
65
105
|
model_config = ConfigDict(extra="forbid")
|
|
66
106
|
|
|
67
|
-
mode: Literal["replace", "disable", "add"]
|
|
68
|
-
path:
|
|
107
|
+
mode: Literal["replace", "patch", "disable", "add"]
|
|
108
|
+
path: PluginOverrideRelativePath | None = None
|
|
109
|
+
base_path: PluginOverrideRelativePath | None = None
|
|
69
110
|
upstream_digest: Sha256Digest | None = None
|
|
70
111
|
on_upstream_change: Literal["warn", "error", "ignore"] | None = None
|
|
71
112
|
|
|
113
|
+
@field_validator("path", "base_path")
|
|
114
|
+
@classmethod
|
|
115
|
+
def validate_override_paths(cls, value: str | None) -> str | None:
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
return _normalize_plugin_override_path(value)
|
|
119
|
+
|
|
72
120
|
@model_validator(mode="after")
|
|
73
121
|
def validate_mode_fields(self) -> PluginSkillOverride:
|
|
74
|
-
if self.mode in {"replace", "add"} and not self.path:
|
|
75
|
-
raise ValueError("path is required when mode is replace or add")
|
|
76
|
-
if self.mode
|
|
77
|
-
raise ValueError(
|
|
122
|
+
if self.mode in {"replace", "patch", "add"} and not self.path:
|
|
123
|
+
raise ValueError("path is required when mode is replace, patch, or add")
|
|
124
|
+
if self.mode in {"replace", "patch"} and self.upstream_digest is None:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"upstream_digest is required when mode is replace or patch"
|
|
127
|
+
)
|
|
128
|
+
if self.mode == "patch" and not self.base_path:
|
|
129
|
+
raise ValueError("base_path is required when mode is patch")
|
|
130
|
+
if self.mode != "patch" and self.base_path is not None:
|
|
131
|
+
raise ValueError("base_path is only valid when mode is patch")
|
|
78
132
|
return self
|
|
79
133
|
|
|
80
134
|
|
|
@@ -84,9 +138,16 @@ class PluginMcpServerOverride(BaseModel):
|
|
|
84
138
|
model_config = ConfigDict(extra="forbid")
|
|
85
139
|
|
|
86
140
|
mode: Literal["patch", "disable", "add"]
|
|
87
|
-
patch_path:
|
|
141
|
+
patch_path: PluginOverrideRelativePath | None = None
|
|
88
142
|
requires_trust_ack: bool = False
|
|
89
143
|
|
|
144
|
+
@field_validator("patch_path")
|
|
145
|
+
@classmethod
|
|
146
|
+
def validate_patch_path(cls, value: str | None) -> str | None:
|
|
147
|
+
if value is None:
|
|
148
|
+
return None
|
|
149
|
+
return _normalize_plugin_override_path(value)
|
|
150
|
+
|
|
90
151
|
@model_validator(mode="after")
|
|
91
152
|
def validate_mode_fields(self) -> PluginMcpServerOverride:
|
|
92
153
|
if self.mode in {"patch", "add"} and not self.patch_path:
|
|
@@ -112,7 +173,9 @@ class PluginOverrideManifest(BaseModel):
|
|
|
112
173
|
|
|
113
174
|
schema_version: Literal[1] = 1
|
|
114
175
|
plugin: PluginComponentName
|
|
115
|
-
components: PluginOverrideComponents = Field(
|
|
176
|
+
components: PluginOverrideComponents = Field(
|
|
177
|
+
default_factory=PluginOverrideComponents
|
|
178
|
+
)
|
|
116
179
|
|
|
117
180
|
|
|
118
181
|
class ReplaceCommandOp(BaseModel):
|
|
@@ -178,6 +241,16 @@ class PluginMcpServerPatch(BaseModel):
|
|
|
178
241
|
ops: list[PluginMcpPatchOperation] = Field(min_length=1)
|
|
179
242
|
|
|
180
243
|
|
|
244
|
+
class PluginAcceptUpstreamProvenance(BaseModel):
|
|
245
|
+
"""Recorded ``overrides accept-upstream`` event for one component."""
|
|
246
|
+
|
|
247
|
+
model_config = ConfigDict(extra="forbid")
|
|
248
|
+
|
|
249
|
+
previous_upstream_digest: Sha256Digest
|
|
250
|
+
new_upstream_digest: Sha256Digest
|
|
251
|
+
accepted_at: str = Field(min_length=1)
|
|
252
|
+
|
|
253
|
+
|
|
181
254
|
class PluginOverrideLockEntry(BaseModel):
|
|
182
255
|
"""Tracked provenance for one consumer-owned plugin override."""
|
|
183
256
|
|
|
@@ -187,8 +260,10 @@ class PluginOverrideLockEntry(BaseModel):
|
|
|
187
260
|
name: PluginComponentName
|
|
188
261
|
mode: Literal["replace", "disable", "add", "patch"]
|
|
189
262
|
local_path: str | None = None
|
|
263
|
+
base_path: str | None = None
|
|
190
264
|
patch_path: str | None = None
|
|
191
265
|
upstream_digest: Sha256Digest | None = None
|
|
266
|
+
last_accept_upstream: PluginAcceptUpstreamProvenance | None = None
|
|
192
267
|
|
|
193
268
|
|
|
194
269
|
class PluginOverrideLock(BaseModel):
|
|
@@ -209,9 +284,9 @@ class PluginEffectiveLockEntry(BaseModel):
|
|
|
209
284
|
|
|
210
285
|
component_kind: Literal["skill", "command", "mcp_server"]
|
|
211
286
|
name: PluginComponentName
|
|
212
|
-
mode: Literal["replace", "disable", "add", "patch"]
|
|
287
|
+
mode: Literal["replace", "disable", "add", "patch", "passthrough"]
|
|
213
288
|
effective_digest: Sha256Digest
|
|
214
|
-
status: Literal["stale"] | None = None
|
|
289
|
+
status: Literal["stale", "merge_conflict"] | None = None
|
|
215
290
|
override_path: str | None = None
|
|
216
291
|
recorded_upstream_digest: Sha256Digest | None = None
|
|
217
292
|
current_base_digest: Sha256Digest | None = None
|
|
@@ -240,10 +315,24 @@ class BootstrapManifest(BaseModel):
|
|
|
240
315
|
model_config = ConfigDict(extra="allow")
|
|
241
316
|
|
|
242
317
|
schema_version: int = Field(ge=1, description="Manifest schema version.")
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
318
|
+
source_kind: Literal["git_overlay", "package"] = Field(
|
|
319
|
+
default="git_overlay",
|
|
320
|
+
description=(
|
|
321
|
+
"Overlay delivery source: 'git_overlay' (a clone checked out at "
|
|
322
|
+
"remote_ref) or 'package' (an installed workstate-system "
|
|
323
|
+
"distribution). Defaults to 'git_overlay' so manifests written "
|
|
324
|
+
"before WS-PKG-DELIVERY-01 validate unchanged."
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
remote_url: str | None = Field(default=None, min_length=1)
|
|
328
|
+
remote_ref: str | None = Field(default=None, min_length=1)
|
|
329
|
+
remote_sha: Sha40 | None = Field(
|
|
330
|
+
default=None,
|
|
331
|
+
description="Resolved 40-char git SHA at install time (git_overlay source).",
|
|
332
|
+
)
|
|
333
|
+
package_version: str | None = Field(
|
|
334
|
+
default=None,
|
|
335
|
+
description="Installed workstate-system distribution version (package source).",
|
|
247
336
|
)
|
|
248
337
|
surfaces: list[OverlaySurface] = Field(default_factory=list)
|
|
249
338
|
configs: list[OverlayConfigEntry] = Field(default_factory=list)
|
|
@@ -261,7 +350,27 @@ class BootstrapManifest(BaseModel):
|
|
|
261
350
|
default=None,
|
|
262
351
|
description=(
|
|
263
352
|
"Optional explicit plugin override root recorded by bootstrap so "
|
|
264
|
-
"later doctor/update/repair runs can reuse a non-default
|
|
353
|
+
"later doctor/update/repair runs can reuse a non-default WORKSTATE-REF-03 "
|
|
265
354
|
"override location."
|
|
266
355
|
),
|
|
267
356
|
)
|
|
357
|
+
|
|
358
|
+
@model_validator(mode="after")
|
|
359
|
+
def _check_source_provenance(self) -> "BootstrapManifest":
|
|
360
|
+
"""Each delivery source requires its own provenance fields.
|
|
361
|
+
|
|
362
|
+
``git_overlay`` (the default, so pre-WS-PKG-DELIVERY-01 manifests keep
|
|
363
|
+
validating) requires the git ``remote_*`` triple; ``package`` requires
|
|
364
|
+
the installed distribution ``package_version``.
|
|
365
|
+
"""
|
|
366
|
+
if self.source_kind == "git_overlay":
|
|
367
|
+
missing = [
|
|
368
|
+
name
|
|
369
|
+
for name in ("remote_url", "remote_ref", "remote_sha")
|
|
370
|
+
if not getattr(self, name)
|
|
371
|
+
]
|
|
372
|
+
if missing:
|
|
373
|
+
raise ValueError("git_overlay manifest requires " + ", ".join(missing))
|
|
374
|
+
elif self.source_kind == "package" and not self.package_version:
|
|
375
|
+
raise ValueError("package manifest requires package_version")
|
|
376
|
+
return self
|
{workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/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.2.1}/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.2.1
|
|
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:
|