Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tests/test_owner_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ def test_finds_files_connectors_skills_and_exports_by_name_only(self) -> None:
self.assertNotIn("secret-value-123", json.dumps(report))
self.assertNotIn("gmail-mcp", json.dumps(report))

def test_malformed_configs_do_not_stop_discovery(self) -> None:
self.write(".claude.json", json.dumps({"mcpServers": ["gmail"], "projects": []}))
(self.home / ".codex").mkdir()
(self.home / ".codex" / "config.toml").write_bytes(b"\xff\xfe")
self.write(".hermes/config.yaml", "mcp_servers:\n slack:\n command: s\n")

report = owner_sources.discover()

self.assertEqual([item["name"] for item in report["connectors"]], ["slack"])

def test_cli_json(self) -> None:
self.write(".claude.json", json.dumps({"mcpServers": {"x": {}}}))
result = subprocess.run(
Expand Down
19 changes: 19 additions & 0 deletions tests/test_skill_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,25 @@ def test_work_only_skill_replaces_persona_handoff(self) -> None:
self.assertNotIn(skill_writer.WORK_ONLY_FALLBACK_ZH, zh_combined)
self.assertNotIn(skill_writer.WORK_ONLY_FALLBACK_EN, en_combined)

def test_work_only_skill_drops_handoff_from_current_work_builder_prompt(self) -> None:
prompt = (Path(__file__).resolve().parents[1] / "prompts" / "shared" / "work_builder.md").read_text(encoding="utf-8")
handoff = next(line for line in prompt.splitlines() if "Persona" in line and line.startswith("If asked"))
zh_handoff = "如果被问到职责范围之外的问题,请按这位同事的方式回应(参见 Persona 部分)。"
self.assertNotIn("Persona", skill_writer.work_only_content(f"## Rules\n\n{handoff}\n", chinese=False)
.replace(skill_writer.WORK_ONLY_FALLBACK_EN, ""))
self.assertNotIn("Persona", skill_writer.work_only_content(f"## 规则\n\n{zh_handoff}\n", chinese=True)
.replace(skill_writer.WORK_ONLY_FALLBACK_ZH, ""))

def test_legacy_self_skill_stays_relationship(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
base_dir = Path(tmp_dir) / "skills" / "relationship"
skill_dir = skill_writer.create_skill(
base_dir, "me", {"name": "Me", "type": "self"}, "Work body", "Persona body"
)
meta = json.loads((skill_dir / "meta.json").read_text(encoding="utf-8"))
self.assertEqual(meta["character"], "relationship")
self.assertEqual(meta["artifacts"]["combined_name"], "relationship-me")

def test_create_celebrity_adds_research_dirs_and_toolchain(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
base_dir = Path(tmp_dir) / "skills" / "celebrity"
Expand Down
15 changes: 10 additions & 5 deletions tools/owner_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,17 @@ def _read_json(path: Path) -> dict:
return data if isinstance(data, dict) else {}


def _server_names(value: object) -> list[str]:
return list(value.keys()) if isinstance(value, dict) else []


def _json_mcp_names(path: Path) -> list[str]:
data = _read_json(path)
names = list((data.get("mcpServers") or {}).keys())
for project in (data.get("projects") or {}).values():
names = _server_names(data.get("mcpServers"))
projects = data.get("projects")
for project in projects.values() if isinstance(projects, dict) else ():
if isinstance(project, dict):
names.extend((project.get("mcpServers") or {}).keys())
names.extend(_server_names(project.get("mcpServers")))
return names


Expand All @@ -184,15 +189,15 @@ def _json_mcp_names(path: Path) -> list[str]:
def _codex_mcp_names(path: Path) -> list[str]:
try:
return _TOML_TABLE.findall(path.read_text(encoding="utf-8"))
except OSError:
except (OSError, ValueError):
return []


def _yaml_section_keys(path: Path, section: str) -> list[str]:
"""Return the direct child keys of a top-level YAML section, without a YAML parser."""
try:
lines = path.read_text(encoding="utf-8").splitlines()
except OSError:
except (OSError, ValueError):
return []
names: list[str] = []
inside = False
Expand Down
10 changes: 10 additions & 0 deletions tools/skill_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,16 @@ def normalize_character(character: str | None) -> str:
return normalized if normalized in CHARACTER_PRESETS else "colleague"


def normalize_skill_character(character: str | None) -> str:
"""Normalize the family stored in a generated skill's metadata.

Generated skills are never owner profiles. Before the owner family existed,
``self`` and ``yourself`` meant ``relationship``, so legacy skills keep that.
"""
normalized = normalize_character(character)
return "relationship" if normalized == "owner" else normalized


def get_character_preset(character: str | None) -> dict:
"""Return the preset for the given character family."""
return CHARACTER_PRESETS[normalize_character(character)]
Expand Down
4 changes: 2 additions & 2 deletions tools/skill_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from skill_presets import (
get_character_preset,
get_research_profile_preset,
normalize_character,
normalize_skill_character,
normalize_research_profile,
)

Expand Down Expand Up @@ -68,7 +68,7 @@ def flatten_legacy_tags(meta: dict) -> list[str]:

def resolve_character(meta: dict, explicit_character: str | None = None) -> str:
"""Resolve the active character family from new or legacy fields."""
return normalize_character(
return normalize_skill_character(
explicit_character
or meta.get("character")
or meta.get("type")
Expand Down
9 changes: 5 additions & 4 deletions tools/skill_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from skill_presets import (
get_character_preset,
normalize_character,
normalize_skill_character,
normalize_research_profile,
resolve_existing_storage_root,
resolve_storage_root,
Expand Down Expand Up @@ -175,10 +176,10 @@ def render_combined_skill(meta: dict, work_content: str, persona_content: str) -


_PERSONA_HANDOFF_PATTERNS = (
re.compile(r"如果被问到职责范围外的问题,以该同事的方式回应(参见 Persona 部分)。\s*"),
re.compile(r"如果被问到[^。\n]*职责范围[^。\n]*Persona[^。\n]*。\s*"),
re.compile(
r"If (?:you are )?asked (?:a question )?outside (?:your|the) "
r"(?:recorded )?responsibilities[^.\n]*Persona[^.\n]*\.\s*",
r"If (?:you are )?asked (?:a question |about something )?outside (?:of )?(?:your|the) "
r"(?:recorded )?(?:scope of )?(?:your )?responsibilit(?:y|ies)[^.\n]*Persona[^.\n]*\.\s*",
re.IGNORECASE,
),
)
Expand Down Expand Up @@ -648,7 +649,7 @@ def main() -> None:
if args.name:
meta["name"] = args.name
meta["display_name"] = args.name
meta["character"] = normalize_character(
meta["character"] = normalize_skill_character(
meta.get("character", meta.get("type", requested_character))
)
meta["research_profile"] = normalize_research_profile(
Expand Down
Loading