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.
Files changed (28) hide show
  1. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/PKG-INFO +2 -2
  2. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/README.md +1 -1
  3. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/pyproject.toml +1 -1
  4. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/__init__.py +1 -7
  5. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/bootstrap.py +129 -20
  6. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/branch_naming.py +13 -13
  7. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/handoff.py +3 -3
  8. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/hooks.py +1 -1
  9. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/paths.py +4 -19
  10. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/skills.py +2 -2
  11. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/PKG-INFO +2 -2
  12. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_bootstrap_manifest.py +1 -1
  13. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_branch_grammar_registry.py +7 -7
  14. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_branch_naming.py +45 -45
  15. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_handoff_schema.py +8 -8
  16. workstate_protocol-0.2.1/tests/test_plugin_override_schema.py +330 -0
  17. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_skill_manifest_real_skills.py +1 -1
  18. workstate_protocol-0.1.6/tests/test_plugin_override_schema.py +0 -145
  19. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/setup.cfg +0 -0
  20. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/compaction.py +0 -0
  21. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/env_aliases.py +0 -0
  22. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol/py.typed +0 -0
  23. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/SOURCES.txt +0 -0
  24. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/dependency_links.txt +0 -0
  25. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/requires.txt +0 -0
  26. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/src/workstate_protocol.egg-info/top_level.txt +0 -0
  27. {workstate_protocol-0.1.6 → workstate_protocol-0.2.1}/tests/test_env_aliases.py +0 -0
  28. {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.6
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 plan 0001)
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 plan 0001)
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.6"
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.6"
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 plan 0001).
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 BaseModel, ConfigDict, Field, StringConstraints, model_validator
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(description="Repo-relative surface path (e.g. '.claude/skills/handoff-lifecycle').")
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 — Plan 0002 step 1), "
37
- "'lifecycle' (Plan 0009 hoisted Make fragment + runner package "
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: str | None = None
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 == "replace" and self.upstream_digest is None:
77
- raise ValueError("upstream_digest is required when mode is replace")
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: str | None = None
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(default_factory=PluginOverrideComponents)
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
- remote_url: str = Field(min_length=1)
244
- remote_ref: str = Field(min_length=1)
245
- remote_sha: Sha40 = Field(
246
- description="Resolved 40-char git SHA at install time.",
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 APD-03 "
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
@@ -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 plan 0006 for context.
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/ahmcp-37-foo``). Task refs in the
13
- Workstate handoff task table are uppercase (``AHMCP-37``).
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
- "BranchClassification",
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/ahmcp-37``,
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/ahmcp-37-branch-naming-enforcement")
73
- ['ahmcp-37-branch-naming-enforcement', 'ahmcp-37']
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 (AHMCP65-BR-02).
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 (Plan 0010 Slice 6)
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 BranchClassification:
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) -> BranchClassification | None:
197
- """Return the :class:`BranchClassification` for ``name`` or ``None``.
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 BranchClassification(kind=entry.kind, branch=name)
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 plan 0001).
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 "E17-14" or "MAINT-7" or "AAA-1".
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. 'E17-14' or 'MAINT-7'."),
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 plan 0001).
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 path were renamed
4
- from the legacy ``.agentic/`` / ``docs/agentic/`` to ``.workstate/`` /
5
- ``docs/workstate/`` (Plan 0013 Slice D, completing Plan 0011 §11 items #3/#4).
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 plan 0001).
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`` (Plan 0002 step 1). The pre-Plan-0002
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workstate-protocol
3
- Version: 0.1.6
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 plan 0001)
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
- AHMCP-50 slice 1b adds a ``mcp_servers: list[str]`` field to the manifest
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
- """Plan 0010 Slice 6 — branch-grammar registry."""
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/ahmcp-37-branch-naming-enforcement", "feature"),
19
+ ("feature/WORKSTATE-37-branch-naming-enforcement", "feature"),
20
20
  ("release/0.9.1", "release"),
21
- ("hotfix/ahmcp-99-fix-bug", "hotfix"),
21
+ ("hotfix/WORKSTATE-99-fix-bug", "hotfix"),
22
22
  ("maint/cleanup-stale-rows", "maint"),
23
- ("revert/ahmcp-12-bad-merge", "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/ahmcp-37-branch-naming-enforcement",
37
+ "feature/WORKSTATE-37-branch-naming-enforcement",
38
38
  "release/0.9.1",
39
- "hotfix/ahmcp-99-fix-bug",
39
+ "hotfix/WORKSTATE-99-fix-bug",
40
40
  "maint/cleanup-stale-rows",
41
- "revert/ahmcp-12-bad-merge",
41
+ "revert/WORKSTATE-12-bad-merge",
42
42
  ],
43
43
  )
44
44
  def test_is_allowed_branch_passes_for_documented_patterns(branch: str) -> None: