diff --git a/openspace/skills/default-utf8-encoding/SKILL.md b/openspace/skills/default-utf8-encoding/SKILL.md new file mode 100644 index 0000000..8ed5239 --- /dev/null +++ b/openspace/skills/default-utf8-encoding/SKILL.md @@ -0,0 +1,20 @@ +--- +name: default-utf8-encoding +description: Baseline file encoding policy. Always use UTF-8 for created or edited text files unless the project explicitly requires another encoding. +--- + +# UTF-8 Baseline + +This is a mandatory baseline skill for local development. + +## Rules + +1. Default encoding for all newly created or edited text files is UTF-8. +2. Preserve existing non-UTF-8 encoding only when the target file is already using it and changing encoding may break runtime behavior. +3. When uncertain about file encoding, prefer safe read/modify/write steps that keep file content valid and avoid mojibake. +4. For JSON, Markdown, YAML, Python, TypeScript, and shell scripts, treat UTF-8 as the standard unless the repository explicitly states otherwise. + +## Output Hygiene + +1. Avoid introducing garbled characters caused by encoding mismatch. +2. Keep line endings and formatting consistent with repository conventions. diff --git a/openspace/tool_layer.py b/openspace/tool_layer.py index 1ea419f..aae9bc1 100644 --- a/openspace/tool_layer.py +++ b/openspace/tool_layer.py @@ -19,6 +19,8 @@ logger = Logger.get_logger(__name__) +MANDATORY_SKILL_NAME = "default-utf8-encoding" + @dataclass class OpenSpaceConfig: @@ -704,6 +706,20 @@ async def _select_and_inject_skills( "selected": [], } + # Always inject built-in UTF-8 baseline guidance when available. + forced_skill_ids: List[str] = [] + forced_meta = self._skill_registry.get_skill_by_name(MANDATORY_SKILL_NAME) + if forced_meta: + already_selected = {s.skill_id for s in selected} + if forced_meta.skill_id not in already_selected: + selected.insert(0, forced_meta) + forced_skill_ids.append(forced_meta.skill_id) + logger.debug(f"Forced baseline skill: {forced_meta.skill_id}") + + if selection_record is not None and forced_skill_ids: + selection_record["forced_skills"] = forced_skill_ids + selection_record["selected"] = [s.skill_id for s in selected] + # Record skill selection to metadata.json if self._recording_manager and selection_record: # Add model info to the record diff --git a/tests/test_utf8_baseline_skill.py b/tests/test_utf8_baseline_skill.py new file mode 100644 index 0000000..4ed0c89 --- /dev/null +++ b/tests/test_utf8_baseline_skill.py @@ -0,0 +1,129 @@ +import importlib +import sys +import types +import unittest + + +def _install_tool_layer_stubs(): + agents_mod = types.ModuleType("openspace.agents") + agents_mod.GroundingAgent = type("GroundingAgent", (), {}) + + llm_mod = types.ModuleType("openspace.llm") + llm_mod.LLMClient = type("LLMClient", (), {"__init__": lambda self, *a, **k: None}) + + grounding_client_mod = types.ModuleType("openspace.grounding.core.grounding_client") + grounding_client_mod.GroundingClient = type("GroundingClient", (), {}) + + config_mod = types.ModuleType("openspace.config") + config_mod.get_config = lambda: None + config_mod.load_config = lambda *args, **kwargs: None + + config_loader_mod = types.ModuleType("openspace.config.loader") + config_loader_mod.get_agent_config = lambda *args, **kwargs: None + + recording_mod = types.ModuleType("openspace.recording") + recording_mod.RecordingManager = type("RecordingManager", (), {}) + + skill_engine_mod = types.ModuleType("openspace.skill_engine") + skill_engine_mod.SkillRegistry = type("SkillRegistry", (), {}) + skill_engine_mod.ExecutionAnalyzer = type("ExecutionAnalyzer", (), {}) + skill_engine_mod.SkillStore = type("SkillStore", (), {}) + + evolver_mod = types.ModuleType("openspace.skill_engine.evolver") + evolver_mod.SkillEvolver = type("SkillEvolver", (), {}) + + logging_mod = types.ModuleType("openspace.utils.logging") + + class Logger: + @staticmethod + def get_logger(name): + return types.SimpleNamespace( + info=lambda *a, **k: None, + debug=lambda *a, **k: None, + warning=lambda *a, **k: None, + error=lambda *a, **k: None, + ) + + logging_mod.Logger = Logger + + stubs = { + "openspace.agents": agents_mod, + "openspace.llm": llm_mod, + "openspace.grounding.core.grounding_client": grounding_client_mod, + "openspace.config": config_mod, + "openspace.config.loader": config_loader_mod, + "openspace.recording": recording_mod, + "openspace.skill_engine": skill_engine_mod, + "openspace.skill_engine.evolver": evolver_mod, + "openspace.utils.logging": logging_mod, + } + + originals = {name: sys.modules.get(name) for name in stubs} + sys.modules.update(stubs) + return originals + + +class UTF8BaselineSkillTests(unittest.IsolatedAsyncioTestCase): + @classmethod + def setUpClass(cls): + cls._originals = _install_tool_layer_stubs() + sys.modules.pop("openspace.tool_layer", None) + cls.tool_layer = importlib.import_module("openspace.tool_layer") + + @classmethod + def tearDownClass(cls): + sys.modules.pop("openspace.tool_layer", None) + for name, module in cls._originals.items(): + if module is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = module + + async def test_select_and_inject_skills_always_includes_utf8_baseline(self): + config = self.tool_layer.OpenSpaceConfig() + openspace = self.tool_layer.OpenSpace(config) + + forced_meta = types.SimpleNamespace( + skill_id="default-utf8-encoding__imp_abc12345", + name="default-utf8-encoding", + description="baseline", + ) + + class FakeRegistry: + async def select_skills_with_llm(self, task, llm_client, max_skills, skill_quality): + return [], {"method": "llm", "selected": []} + + def get_skill_by_name(self, name): + if name == "default-utf8-encoding": + return forced_meta + return None + + def build_context_injection(self, skills, backends=None): + return "|".join(s.skill_id for s in skills) + + injected = {} + + class FakeAgent: + backend_scope = ["shell"] + + def set_skill_context(self, context_text, skill_ids): + injected["context_text"] = context_text + injected["skill_ids"] = list(skill_ids) + + def clear_skill_context(self): + injected["cleared"] = True + + openspace._skill_registry = FakeRegistry() + openspace._grounding_agent = FakeAgent() + openspace._recording_manager = None + openspace._skill_store = None + openspace._get_skill_selection_llm = lambda: object() + + selected = await openspace._select_and_inject_skills("any task") + + self.assertTrue(selected) + self.assertIn(forced_meta.skill_id, injected["skill_ids"]) + + +if __name__ == "__main__": + unittest.main()