From 6406c4e23a803943f82a635400cebcf3f11fd370 Mon Sep 17 00:00:00 2001 From: OUAO-FRANK <3295933938@qq.com> Date: Thu, 29 Oct 2026 00:27:19 +0800 Subject: [PATCH] Accept quoted skill names during tree validation --- harness2/core/tree.py | 5 ++- tests/test_skill_names.py | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 tests/test_skill_names.py diff --git a/harness2/core/tree.py b/harness2/core/tree.py index c9bf1341..8402f060 100644 --- a/harness2/core/tree.py +++ b/harness2/core/tree.py @@ -275,14 +275,15 @@ def _check_skills(surface: SurfaceSpec, tree: Tree, body: str, errs: list[str]) f"frontmatter, or the skill is dropped SILENTLY at load" ) m_name = re.search(r"^name:\s*(\S+)\s*$", fm, re.M) + matching_names = {name, f'"{name}"', f"'{name}'"} if surface.skill_name_rule == "must_match": - if not (m_name and m_name.group(1) == name): + if not (m_name and m_name.group(1) in matching_names): errs.append( f"{surface.skill_dir}/{name}/SKILL.md needs frontmatter " f"`name: {name}` matching its directory (a mismatch is dropped " f"SILENTLY at load)" ) - elif m_name and m_name.group(1) != name: + elif m_name and m_name.group(1) not in matching_names: errs.append( f"{surface.skill_dir}/{name}/SKILL.md frontmatter `name: " f"{m_name.group(1)}` OVERRIDES the directory name at load, so every " diff --git a/tests/test_skill_names.py b/tests/test_skill_names.py new file mode 100644 index 00000000..cef66be3 --- /dev/null +++ b/tests/test_skill_names.py @@ -0,0 +1,77 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Skill name spelling must agree with the substrate's YAML frontmatter value.""" + +import unittest +from unittest.mock import Mock + +from harness2.core import recursion, tree +from harness2.substrate.codex import CODEX_SURFACE +from harness2.substrate.opencode import OPENCODE_SURFACE + + +class SkillNameTests(unittest.TestCase): + def candidate(self, surface, name_line): + return { + surface.always_on[0]: "Use invoice-review when reviewing invoices.", + f"{surface.skill_dir}/invoice-review/SKILL.md": ( + f"---\n{name_line}description: Review invoices.\n---\nCheck invoice totals.\n" + ), + } + + def test_matching_name_spellings_are_accepted(self): + for surface in (CODEX_SURFACE, OPENCODE_SURFACE): + for value in ("invoice-review", '"invoice-review"', "'invoice-review'"): + with self.subTest(surface=surface.name, value=value): + census = tree.validate(surface, self.candidate(surface, f"name: {value}\n")) + self.assertEqual(census["skills"], 1) + + def test_other_names_and_unbalanced_quotes_are_rejected(self): + for surface in (CODEX_SURFACE, OPENCODE_SURFACE): + for value in ( + "other-skill", + '"other-skill"', + "'other-skill'", + '"invoice-review', + "invoice-review'", + "\"invoice-review'", + ): + with self.subTest(surface=surface.name, value=value): + with self.assertRaisesRegex(ValueError, "frontmatter"): + tree.validate(surface, self.candidate(surface, f"name: {value}\n")) + + def test_name_remains_optional_only_for_codex(self): + self.assertEqual( + tree.validate(CODEX_SURFACE, self.candidate(CODEX_SURFACE, ""))["skills"], 1 + ) + with self.assertRaisesRegex(ValueError, "matching its directory"): + tree.validate(OPENCODE_SURFACE, self.candidate(OPENCODE_SURFACE, "")) + + def test_quoted_skill_is_accepted_as_a_proposal(self): + adapter = Mock() + adapter.spec.allow_subagents = True + adapter.spec.reserved_scripts = frozenset() + candidate = self.candidate(CODEX_SURFACE, 'name: "invoice-review"\n') + fallback = {"AGENTS.md": candidate["AGENTS.md"]} + + kept, error = recursion._accept_tree(CODEX_SURFACE, adapter, candidate, fallback, "inv1") + + self.assertFalse(kept) + self.assertEqual(error, "") + adapter.validate_tree.assert_called_once_with(candidate) + + +if __name__ == "__main__": + unittest.main()