diff --git a/agent/coding_context.py b/agent/coding_context.py index 34781cfa1d..b534b6da32 100644 --- a/agent/coding_context.py +++ b/agent/coding_context.py @@ -38,12 +38,13 @@ session (deferred), the same contract as ``/skills install`` vs ``--now``. Activation (config ``agent.coding_context``): - * ``auto`` (default) — posture (brief + snapshot + names-only demotion of - non-coding skill categories) on an interactive coding surface sitting in - a code workspace (git repo or recognised project root). Prompt-only; - toolsets untouched, no skill is ever hidden. + * ``auto`` (default) — posture (brief + snapshot) on an interactive coding + surface sitting in a code workspace (git repo or recognised project root). + Prompt-only; toolsets and the skill index untouched. * ``focus`` — like ``auto``, but additionally collapses the toolset to the - ``coding`` set + enabled MCP servers. Explicit opt-in for a lean schema. + ``coding`` set + enabled MCP servers and demotes non-coding skill + categories to names-only in the prompt's skill index (no skill is ever + hidden). Explicit opt-in for a lean schema. * ``on`` — force the posture anywhere (incl. non-workspaces). Prompt-only. * ``off`` — disable entirely. """ @@ -214,8 +215,8 @@ class ContextProfile: (extension seam; not yet consumed by the router). ``memory_policy``— memory namespace/weighting hint (extension seam). ``compact_skill_categories`` — skill categories DEMOTED to names-only in - the system-prompt skill index while this posture is - active. Never hidden: every skill name stays visible + the system-prompt skill index under the opt-in ``focus`` + mode. Never hidden: every skill name stays visible (so memory-anchored recall keeps working) — only the descriptions are dropped to cut index noise. Deny-list semantics so unknown/custom categories keep full @@ -231,7 +232,7 @@ class ContextProfile: # Skill categories that are clearly not part of a coding workflow. Demoted to -# names-only in the prompt's skill index while the coding posture is active +# names-only in the prompt's skill index under the opt-in ``focus`` mode only # (deny-list — anything not listed here, incl. custom user categories, keeps # full entries). Coding-adjacent categories (devops, github, mcp, # data-science, diagramming, research, security, …) are intentionally absent. @@ -438,15 +439,22 @@ class RuntimeMode: def compact_skill_categories(self) -> frozenset[str]: """Skill categories to demote to names-only in the prompt's skill index. - Demoted — never hidden. An earlier revision fully pruned these - categories from the index, which caused silent capability loss in a - real workflow: agent-created skills are the model's accumulated - project memory (server-ops runbooks, learned pitfalls, …), and models - do not reliably reach for ``skills_list`` to rediscover what the - index stopped showing them. Names-only keeps every skill loadable on - recall while still cutting the description noise from the index. + Gated on the opt-in ``focus`` mode, like the toolset collapse: the + default posture leaves the skill index untouched. Users who didn't ask + for a lean prompt keep full entries for every category — index changes + under ``auto`` proved too surprising in practice, even names-only ones + (a demoted description is information the model no longer weighs when + deciding what to load). + + Demoted — never hidden — even under ``focus``. An earlier revision + fully pruned these categories from the index, which caused silent + capability loss in a real workflow: agent-created skills are the + model's accumulated project memory (server-ops runbooks, learned + pitfalls, …), and models do not reliably reach for ``skills_list`` to + rediscover what the index stopped showing them. Names-only keeps every + skill loadable on recall while still cutting the description noise. """ - if not self.is_coding: + if not self.is_coding or self.config_mode != "focus": return frozenset() return frozenset(self.profile.compact_skill_categories) @@ -534,9 +542,11 @@ def coding_compact_skill_categories( ) -> frozenset[str]: """Skill categories the active posture demotes to names-only in the index. - Empty outside the coding posture. Demoted — never hidden: every skill - name stays in the index and remains loadable via ``skill_view`` / - ``skills_list``; only descriptions are dropped. + Empty outside the coding posture and outside the opt-in ``focus`` mode — + the default posture never touches the skill index. Under ``focus``, + demoted — never hidden: every skill name stays in the index and remains + loadable via ``skill_view`` / ``skills_list``; only descriptions are + dropped. """ return resolve_runtime_mode( platform=platform, cwd=cwd, config=config diff --git a/agent/system_prompt.py b/agent/system_prompt.py index 7721536c6d..76f57dfcdb 100644 --- a/agent/system_prompt.py +++ b/agent/system_prompt.py @@ -191,9 +191,10 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None) ) if toolset } - # Coding posture demotes non-coding skill categories to names-only in - # the index (never hidden — skill_view/skills_list reach everything, - # and every name stays visible for memory-anchored recall). + # Focus mode (opt-in) demotes non-coding skill categories to + # names-only in the index (never hidden — skill_view/skills_list + # reach everything, and every name stays visible for recall). The + # default coding posture leaves the index untouched. _compact_cats = frozenset() try: from agent.coding_context import coding_compact_skill_categories diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 1605527935..65a618e78f 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -877,7 +877,9 @@ DEFAULT_CONFIG = { # Toolsets are never touched; messaging platforms # unaffected. # "focus" — auto + collapse the toolset to the lean coding - # set (+ enabled MCP servers). Explicit opt-in. + # set (+ enabled MCP servers) + demote non-coding + # skill categories to names-only in the prompt's + # skill index. Explicit opt-in. # "on" — force the prompt posture everywhere. # "off" — disable entirely. "coding_context": "auto", diff --git a/tests/agent/test_coding_context.py b/tests/agent/test_coding_context.py index 6f7812100c..36937dc871 100644 --- a/tests/agent/test_coding_context.py +++ b/tests/agent/test_coding_context.py @@ -368,21 +368,27 @@ class TestProfiles: assert cc.GENERAL_PROFILE.toolset is None assert cc.GENERAL_PROFILE.guidance == "" - def test_skill_demotion_scoped_to_coding_posture(self, tmp_path): - # Coding posture demotes clearly-non-coding categories to names-only - # in the index (never hides them — agent-created skills are the - # model's project memory and must stay recallable by name). - # Coding-adjacent categories keep full entries (deny-list semantics). + def test_skill_demotion_gated_on_focus(self, tmp_path): + # Names-only demotion is opt-in via focus mode — the default (auto) + # and forced (on) postures leave the skill index untouched. Under + # focus, clearly-non-coding categories are demoted (never hidden) and + # coding-adjacent ones keep full entries (deny-list semantics). _git_init(tmp_path) - for raw in ("auto", "on", "focus"): + for raw in ("auto", "on"): mode = cc.resolve_runtime_mode( platform="cli", cwd=tmp_path, config={"agent": {"coding_context": raw}} ) assert mode.is_coding is True - compact = mode.compact_skill_categories() - assert "social-media" in compact and "smart-home" in compact - for kept in ("github", "devops", "software-development", "data-science"): - assert kept not in compact + assert mode.compact_skill_categories() == frozenset() + focus = cc.resolve_runtime_mode( + platform="cli", cwd=tmp_path, + config={"agent": {"coding_context": "focus"}}, + ) + assert focus.is_coding is True + compact = focus.compact_skill_categories() + assert "social-media" in compact and "smart-home" in compact + for kept in ("github", "devops", "software-development", "data-science"): + assert kept not in compact # General posture demotes nothing. general = cc.resolve_runtime_mode(platform="telegram", cwd=tmp_path, config={}) assert general.compact_skill_categories() == frozenset()