diff --git a/skills/continuous-learning-v2/scripts/instinct-cli.py b/skills/continuous-learning-v2/scripts/instinct-cli.py index d6b0820cd..57fe9a5d9 100755 --- a/skills/continuous-learning-v2/scripts/instinct-cli.py +++ b/skills/continuous-learning-v2/scripts/instinct-cli.py @@ -238,11 +238,10 @@ def _fetch_import_url(source: str, *, max_bytes: int = 2 * 1024 * 1024) -> str: def _yaml_quote(value: str) -> str: """Quote a string for safe YAML frontmatter serialization. - Uses double quotes and escapes embedded double-quote characters to - prevent malformed YAML when the value contains quotes. + JSON double-quoted strings are valid YAML and keep control characters + escaped on one line, preserving instinct frontmatter boundaries. """ - escaped = value.replace('\\', '\\\\').replace('"', '\\"') - return f'"{escaped}"' + return json.dumps(value) # ───────────────────────────────────────────── @@ -546,7 +545,10 @@ def parse_instinct_file(content: str) -> list[dict]: value = value.strip() # Unescape quoted YAML strings if value.startswith('"') and value.endswith('"'): - value = value[1:-1].replace('\\"', '"').replace('\\\\', '\\') + try: + value = json.loads(value) + except json.JSONDecodeError: + value = value[1:-1].replace('\\"', '"').replace('\\\\', '\\') elif value.startswith("'") and value.endswith("'"): value = value[1:-1].replace("''", "'") if key == 'confidence': diff --git a/skills/continuous-learning-v2/scripts/test_parse_instinct.py b/skills/continuous-learning-v2/scripts/test_parse_instinct.py index f1c65f0c6..76733362b 100644 --- a/skills/continuous-learning-v2/scripts/test_parse_instinct.py +++ b/skills/continuous-learning-v2/scripts/test_parse_instinct.py @@ -281,6 +281,36 @@ def test_parse_confidence_preserves_valid_range(confidence): assert parse_instinct_file(content)[0]["confidence"] == confidence +@pytest.mark.parametrize("value", [ + "line one\nline two", "tab\tvalue", "carriage\rreturn", + 'when writing "quoted" text', r"C:\Users\name", "caf\u00e9", + r"literal\ntext", "line\n---\nid: other\n---", "control\x01value", +]) +def test_quoted_instinct_strings_roundtrip(value: str) -> None: + quoted = _mod._yaml_quote(value) + assert json.loads(quoted) == value + instincts = parse_instinct_file(f"---\nid: roundtrip\ntrigger: {quoted}\n---\nBody") + assert len(instincts) == 1 + assert instincts[0]["trigger"] == value + + +def test_parse_quoted_instinct_decodes_newline_escape() -> None: + content = '---\nid: newline\ntrigger: "line one\\nline two"\n---\nBody' + assert parse_instinct_file(content)[0]["trigger"] == "line one\nline two" + + +@pytest.mark.parametrize("value", ["line\u0085value", "line\u0090value"]) +def test_unicode_controls_are_escaped_for_yaml(value: str) -> None: + quoted = _mod._yaml_quote(value) + assert quoted.isascii() + assert json.loads(quoted) == value + + +def test_parse_quoted_instinct_preserves_legacy_escape() -> None: + content = '---\nid: legacy\ntrigger: "C:\\projects"\n---\nBody' + assert parse_instinct_file(content)[0]["trigger"] == r"C:\projects" + + def test_parse_trigger_strips_quotes(): content = """\ ---