generated from coulomb/repo-seed
session-memory: Claude + Codex + Grok distributors + registry (WP-0007 T02/T03)
Thin per-flavor distributors over the shared base: Claude (CLAUDE.md, optional skill-stub mode), Codex (AGENTS.md), Grok (.grok/instructions.md). registry maps flavor->distributor — adding a flavor is one entry + one module. Same agnostic body renders to distinct per-flavor targets (FR-A3). 7 new tests; suite 117/117. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
42
session_memory/distribute/claude.py
Normal file
42
session_memory/distribute/claude.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""Claude distributor (PRD §6.4 FR-X1; T02).
|
||||||
|
|
||||||
|
Renders an approved Solution Pattern into a ``CLAUDE.md`` snippet block. Most logic
|
||||||
|
is inherited from :class:`BaseDistributor`; the Claude-specific touch is an
|
||||||
|
optional **skill** rendering mode (``rendering_hints["claude"]["as"] == "skill"``)
|
||||||
|
that emits a skill-style stub instead of a plain instruction snippet — Claude's
|
||||||
|
native distribution targets are CLAUDE.md snippets, skills, or hooks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ..curate.schema import SolutionPattern
|
||||||
|
from .base import BaseDistributor, hint, render_markdown_body
|
||||||
|
|
||||||
|
|
||||||
|
class ClaudeDistributor(BaseDistributor):
|
||||||
|
flavor = "claude"
|
||||||
|
target_path = "CLAUDE.md"
|
||||||
|
|
||||||
|
def body(self, pattern: SolutionPattern) -> str:
|
||||||
|
override = hint(pattern, self.flavor, "body")
|
||||||
|
if override:
|
||||||
|
return override
|
||||||
|
if hint(pattern, self.flavor, "as") == "skill":
|
||||||
|
return self._skill_stub(pattern)
|
||||||
|
return render_markdown_body(pattern)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _skill_stub(pattern: SolutionPattern) -> str:
|
||||||
|
trigger = "avoid" if pattern.polarity == "problem" else "apply"
|
||||||
|
lines = [
|
||||||
|
f"## Skill: {pattern.name}",
|
||||||
|
"",
|
||||||
|
f"**When:** situations where you would {trigger} — {pattern.problem.strip()}",
|
||||||
|
"",
|
||||||
|
"**Steps:**",
|
||||||
|
]
|
||||||
|
for r in pattern.resolutions:
|
||||||
|
lines.append(f"- {r.summary}" + (f" — {r.detail}" if r.detail else ""))
|
||||||
|
for step in r.steps:
|
||||||
|
lines.append(f" - {step}")
|
||||||
|
return "\n".join(lines).strip()
|
||||||
15
session_memory/distribute/codex.py
Normal file
15
session_memory/distribute/codex.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""Codex distributor (PRD §6.4 FR-X1; T03).
|
||||||
|
|
||||||
|
Renders an approved Solution Pattern into an ``AGENTS.md`` snippet — Codex's native
|
||||||
|
repo-convention surface. Identical agnostic body to the other flavors (FR-A3: one
|
||||||
|
pattern, expressible everywhere); only the target file differs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .base import BaseDistributor
|
||||||
|
|
||||||
|
|
||||||
|
class CodexDistributor(BaseDistributor):
|
||||||
|
flavor = "codex"
|
||||||
|
target_path = "AGENTS.md"
|
||||||
15
session_memory/distribute/grok.py
Normal file
15
session_memory/distribute/grok.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""Grok distributor (PRD §6.4 FR-X1; T03).
|
||||||
|
|
||||||
|
Renders an approved Solution Pattern into Grok's native instruction format. Defaults
|
||||||
|
to a ``.grok/instructions.md`` snippet; the same agnostic body as the other flavors
|
||||||
|
(FR-A3), overridable via ``rendering_hints["grok"]``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .base import BaseDistributor
|
||||||
|
|
||||||
|
|
||||||
|
class GrokDistributor(BaseDistributor):
|
||||||
|
flavor = "grok"
|
||||||
|
target_path = ".grok/instructions.md"
|
||||||
26
session_memory/distribute/registry.py
Normal file
26
session_memory/distribute/registry.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""Distributor registry (T03) — flavor -> distributor, the one place that knows
|
||||||
|
about all flavor edges. Adding a flavor = one entry here + one adapter module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .base import BaseDistributor
|
||||||
|
from .claude import ClaudeDistributor
|
||||||
|
from .codex import CodexDistributor
|
||||||
|
from .grok import GrokDistributor
|
||||||
|
|
||||||
|
_REGISTRY: dict[str, BaseDistributor] = {
|
||||||
|
"claude": ClaudeDistributor(),
|
||||||
|
"codex": CodexDistributor(),
|
||||||
|
"grok": GrokDistributor(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_distributor(flavor: str) -> Optional[BaseDistributor]:
|
||||||
|
return _REGISTRY.get(flavor)
|
||||||
|
|
||||||
|
|
||||||
|
def all_flavors() -> list[str]:
|
||||||
|
return list(_REGISTRY)
|
||||||
40
tests/test_distribute_claude.py
Normal file
40
tests/test_distribute_claude.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Claude distributor tests (WP-0007 T02)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from session_memory.curate.schema import Resolution, SolutionPattern # noqa: E402
|
||||||
|
from session_memory.distribute.claude import ClaudeDistributor # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _pattern(hints=None):
|
||||||
|
return SolutionPattern(
|
||||||
|
id="sp-read-before-edit", name="Read before edit", version="1.0.0",
|
||||||
|
polarity="problem", problem="Agents edit files they have not read.",
|
||||||
|
resolutions=[Resolution(summary="Read the file first", steps=["Read", "Edit"])],
|
||||||
|
rendering_hints=hints or {"claude": {}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_targets_claude_md():
|
||||||
|
art = ClaudeDistributor().render(_pattern())
|
||||||
|
assert art.flavor == "claude"
|
||||||
|
assert art.target_path == "CLAUDE.md"
|
||||||
|
assert "BEGIN helix-forge pattern:sp-read-before-edit" in art.content
|
||||||
|
assert "### Read before edit" in art.content
|
||||||
|
|
||||||
|
|
||||||
|
def test_skill_mode_emits_skill_stub():
|
||||||
|
art = ClaudeDistributor().render(_pattern({"claude": {"as": "skill"}}))
|
||||||
|
assert "## Skill: Read before edit" in art.content
|
||||||
|
assert "**When:**" in art.content
|
||||||
|
assert " - Read" in art.content
|
||||||
|
|
||||||
|
|
||||||
|
def test_idempotent_marker_present_for_reupsert():
|
||||||
|
art = ClaudeDistributor().render(_pattern())
|
||||||
|
# same id in both renders -> caller can upsert in place
|
||||||
|
art2 = ClaudeDistributor().render(_pattern())
|
||||||
|
assert art.pattern_id == art2.pattern_id == "sp-read-before-edit"
|
||||||
49
tests/test_distribute_codex_grok.py
Normal file
49
tests/test_distribute_codex_grok.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""Codex + Grok distributor + registry tests (WP-0007 T03)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from session_memory.curate.schema import Resolution, SolutionPattern # noqa: E402
|
||||||
|
from session_memory.distribute.codex import CodexDistributor # noqa: E402
|
||||||
|
from session_memory.distribute.grok import GrokDistributor # noqa: E402
|
||||||
|
from session_memory.distribute.registry import all_flavors, get_distributor # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _pattern():
|
||||||
|
return SolutionPattern(
|
||||||
|
id="sp-x", name="Read before edit", version="1.0.0", polarity="problem",
|
||||||
|
problem="Agents edit files they have not read.",
|
||||||
|
resolutions=[Resolution(summary="Read the file first")],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_targets_agents_md():
|
||||||
|
art = CodexDistributor().render(_pattern())
|
||||||
|
assert art.flavor == "codex" and art.target_path == "AGENTS.md"
|
||||||
|
assert "Read before edit" in art.content
|
||||||
|
|
||||||
|
|
||||||
|
def test_grok_targets_native_instructions():
|
||||||
|
art = GrokDistributor().render(_pattern())
|
||||||
|
assert art.flavor == "grok" and art.target_path == ".grok/instructions.md"
|
||||||
|
|
||||||
|
|
||||||
|
def test_same_pattern_expressible_for_all_flavors():
|
||||||
|
# FR-A3: one pattern, rendered for every flavor (same body, different targets)
|
||||||
|
p = _pattern()
|
||||||
|
bodies = {}
|
||||||
|
for f in all_flavors():
|
||||||
|
art = get_distributor(f).render(p)
|
||||||
|
# strip markers -> compare agnostic body
|
||||||
|
inner = art.content.split("\n", 1)[1].rsplit("\n", 1)[0]
|
||||||
|
bodies[f] = inner
|
||||||
|
targets = {get_distributor(f).render(p).target_path for f in all_flavors()}
|
||||||
|
assert len(targets) == 3 # distinct per-flavor targets
|
||||||
|
assert len(set(bodies.values())) == 1 # identical agnostic body
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_unknown_flavor():
|
||||||
|
assert get_distributor("gpt") is None
|
||||||
|
assert set(all_flavors()) == {"claude", "codex", "grok"}
|
||||||
@@ -44,7 +44,7 @@ adapters. Unit-tested.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: AGENTIC-WP-0007-T02
|
id: AGENTIC-WP-0007-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "64f50bd4-1fdf-452e-ae14-890253ab9f33"
|
state_hub_task_id: "64f50bd4-1fdf-452e-ae14-890253ab9f33"
|
||||||
```
|
```
|
||||||
@@ -57,7 +57,7 @@ place rather than duplicating. Uses `rendering_hints["claude"]`. Unit-tested.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: AGENTIC-WP-0007-T03
|
id: AGENTIC-WP-0007-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "382790f5-1fb4-4394-b039-1649cbf3b20a"
|
state_hub_task_id: "382790f5-1fb4-4394-b039-1649cbf3b20a"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user