instructions: the agent speaks the KB language in chat - AGENTS.md § Conversation language, and instructions sync compiles the language tag into every published SKILL.md (#116); 10.0.0-beta.9
Files changed: - AGENTS.md - CHANGES.md - VERSION - docs/explanation/language-boundaries.md - docs/reference/commands/types-instructions-and-docs.md - instructions/CONTRACT.md - instructions/dev/stack-build/SKILL.md - instructions/dev/stack-close/SKILL.md - instructions/dev/stack-dev/SKILL.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - tools/CONTRACT.md - tools/chemenu/commands/doctor.py - tools/chemenu/commands/instructions_cmd.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_instructions_cmd.py Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
c06bde2e95
commit
0f0a5799fd
16 files changed
+385
-54
No files matched your search
@@ -461,6 +461,22 @@ def test_unpublished_skills_fail(instance):
|
||||
assert _status(checks, "skills") == "FAIL"
|
||||
|
||||
|
||||
def test_a_changed_conversation_language_fails_skills_and_names_the_block(instance):
|
||||
(config.KB_DIR / conventions.CONVENTIONS_FILENAME).write_text(
|
||||
"---\nlanguage: de\nprofile: none\nsections:\n links: Relationships\n"
|
||||
" footnotes: Footnotes\n---\n\n# conventions\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
conventions.reset_cache()
|
||||
checks = doctor.run_doctor()
|
||||
assert _status(checks, "skills") == "FAIL"
|
||||
assert "2 published copy/copies drifted" in _detail(checks, "skills")
|
||||
assert "language block is for `en`, conventions say `de`" in _detail(checks, "skills")
|
||||
|
||||
instructions_cmd.sync(force=False)
|
||||
assert _status(doctor.run_doctor(), "skills") == "OK"
|
||||
|
||||
|
||||
def test_no_author_fails(instance, monkeypatch):
|
||||
monkeypatch.delenv("WIKI_AUTHOR", raising=False)
|
||||
monkeypatch.setattr(config, "default_author", lambda: None)
|
||||
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
import typer
|
||||
|
||||
from chemenu import config
|
||||
from chemenu import config, conventions
|
||||
from chemenu.tests.conftest import use_shipped_type_specs
|
||||
from chemenu.commands import instructions_cmd
|
||||
|
||||
@@ -230,6 +230,184 @@ def test_drift_detects_missing_edited_and_extra(layer):
|
||||
assert "extra" in instructions_cmd.drift(source, target)
|
||||
|
||||
|
||||
# --- conversation language block --------------------------------------------
|
||||
|
||||
|
||||
def _declare_language(root: Path, tag: str | None) -> None:
|
||||
path = root / "kb" / "CONVENTIONS.md"
|
||||
if tag is None:
|
||||
path.unlink(missing_ok=True)
|
||||
else:
|
||||
path.write_text(f"---\nlanguage: {tag}\nprofile: none\n---\n\n# conventions\n", encoding="utf-8")
|
||||
conventions.reset_cache()
|
||||
|
||||
|
||||
def _rich_skill(layer) -> Path:
|
||||
"""A skill whose frontmatter has a flag and a value holding `---`, plus a sibling file."""
|
||||
source = layer / "instructions" / "wiki-demo"
|
||||
(source / "SKILL.md").write_text(
|
||||
"---\nname: wiki-demo\ndescription: Demo --- skill.\ndisable-model-invocation: true\n---\n\n# Demo\n\nSee gates.md.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(source / "reference.md").write_text("# reference\n", encoding="utf-8")
|
||||
return source
|
||||
|
||||
|
||||
def _block_lines(text: str) -> list[str]:
|
||||
return [line for line in text.splitlines() if line.startswith("> **Conversation language:")]
|
||||
|
||||
|
||||
def test_sync_writes_the_language_block_directly_under_the_frontmatter_in_both_targets(layer):
|
||||
source = _rich_skill(layer)
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
|
||||
source_text = (source / "SKILL.md").read_text(encoding="utf-8")
|
||||
frontmatter_end = source_text.index("---\n\n# Demo") + len("---\n")
|
||||
for harness in (".agents", ".claude"):
|
||||
copy = _skill_copy(layer, harness)
|
||||
text = (copy / "SKILL.md").read_text(encoding="utf-8")
|
||||
assert len(_block_lines(text)) == 1
|
||||
assert text[:frontmatter_end] == source_text[:frontmatter_end]
|
||||
after = text[frontmatter_end:].split("\n")
|
||||
assert after[0] == "" and after[1].startswith("> **Conversation language: `de`**")
|
||||
assert "`AGENTS.md` § Conversation language" in after[1]
|
||||
assert text.replace(after[1] + "\n\n", "", 1) == source_text
|
||||
assert (copy / "reference.md").read_bytes() == (source / "reference.md").read_bytes()
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".agents")) is None
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".claude")) is None
|
||||
|
||||
|
||||
def test_the_block_is_the_tag_and_never_a_language_name(layer):
|
||||
_declare_language(layer, "pt-BR")
|
||||
instructions_cmd.sync(force=False)
|
||||
text = (_skill_copy(layer, ".claude") / "SKILL.md").read_text(encoding="utf-8")
|
||||
assert "`pt-BR`" in _block_lines(text)[0]
|
||||
assert "Portuguese" not in text
|
||||
|
||||
|
||||
def test_a_second_sync_is_byte_identical_and_adds_no_second_block(layer):
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
first = (_skill_copy(layer, ".agents") / "SKILL.md").read_bytes()
|
||||
instructions_cmd.sync(force=False)
|
||||
second = (_skill_copy(layer, ".agents") / "SKILL.md").read_bytes()
|
||||
assert first == second
|
||||
assert len(_block_lines(second.decode("utf-8"))) == 1
|
||||
|
||||
|
||||
def test_without_a_declared_language_every_copy_is_the_source(layer, capsys):
|
||||
source = _rich_skill(layer)
|
||||
_declare_language(layer, None)
|
||||
instructions_cmd.sync(force=False)
|
||||
assert "no language declared" in " ".join(capsys.readouterr().out.split())
|
||||
for harness in (".agents", ".claude"):
|
||||
copy = _skill_copy(layer, harness)
|
||||
assert (copy / "SKILL.md").read_bytes() == (source / "SKILL.md").read_bytes()
|
||||
assert instructions_cmd.drift(source, copy) is None
|
||||
|
||||
|
||||
def test_sync_names_the_language_it_stamped(layer, capsys):
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
assert "conversation language `de`" in " ".join(capsys.readouterr().out.split())
|
||||
|
||||
|
||||
def test_a_skill_without_closed_frontmatter_is_copied_unchanged(layer):
|
||||
source = layer / "instructions" / "wiki-demo"
|
||||
(source / "SKILL.md").write_text("---\nname: wiki-demo\n# never closed\n", encoding="utf-8")
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
assert (_skill_copy(layer, ".claude") / "SKILL.md").read_bytes() == (source / "SKILL.md").read_bytes()
|
||||
|
||||
|
||||
def test_a_crlf_skill_keeps_its_line_endings(layer):
|
||||
source = layer / "instructions" / "wiki-demo"
|
||||
(source / "SKILL.md").write_bytes(b"---\r\nname: wiki-demo\r\ndescription: Demo.\r\n---\r\n\r\n# Demo\r\n")
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
data = (_skill_copy(layer, ".claude") / "SKILL.md").read_bytes()
|
||||
assert data.count(b"\n") == data.count(b"\r\n")
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".claude")) is None
|
||||
|
||||
|
||||
def test_changing_the_language_after_sync_is_drift_that_names_the_block(layer):
|
||||
source = layer / "instructions" / "wiki-demo"
|
||||
_declare_language(layer, "en")
|
||||
instructions_cmd.sync(force=False)
|
||||
_declare_language(layer, "de")
|
||||
for harness in (".agents", ".claude"):
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, harness)) == (
|
||||
"language block is for `en`, conventions say `de`"
|
||||
)
|
||||
instructions_cmd.sync(force=False)
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".agents")) is None
|
||||
|
||||
|
||||
def test_declaring_a_language_after_a_plain_sync_is_drift_that_names_the_block(layer):
|
||||
source = layer / "instructions" / "wiki-demo"
|
||||
instructions_cmd.sync(force=False)
|
||||
_declare_language(layer, "de")
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".agents")) == (
|
||||
"language block missing, conventions say `de`"
|
||||
)
|
||||
|
||||
|
||||
def test_dropping_the_language_after_sync_is_drift_that_names_the_block(layer):
|
||||
source = layer / "instructions" / "wiki-demo"
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
_declare_language(layer, None)
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".agents")) == (
|
||||
"language block is for `de`, conventions declare no language"
|
||||
)
|
||||
|
||||
|
||||
def test_a_hand_edit_next_to_the_block_is_still_reported_as_a_difference(layer):
|
||||
source = layer / "instructions" / "wiki-demo"
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
target = _skill_copy(layer, ".agents") / "SKILL.md"
|
||||
target.write_text(target.read_text(encoding="utf-8") + "extra\n", encoding="utf-8")
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".agents")) == "differs in SKILL.md"
|
||||
|
||||
|
||||
def test_a_hand_edit_of_another_file_is_still_reported(layer):
|
||||
source = _rich_skill(layer)
|
||||
_declare_language(layer, "de")
|
||||
instructions_cmd.sync(force=False)
|
||||
(_skill_copy(layer, ".claude") / "reference.md").write_text("# edited\n", encoding="utf-8")
|
||||
assert instructions_cmd.drift(source, _skill_copy(layer, ".claude")) == "differs in reference.md"
|
||||
|
||||
|
||||
def test_verify_reports_a_language_change_per_copy_and_sync_repairs_it(layer, capsys):
|
||||
_declare_language(layer, "en")
|
||||
instructions_cmd.sync(force=False)
|
||||
instructions_cmd.verify()
|
||||
capsys.readouterr()
|
||||
|
||||
_declare_language(layer, "de")
|
||||
with pytest.raises(typer.Exit):
|
||||
instructions_cmd.verify()
|
||||
out = capsys.readouterr().out
|
||||
for harness in (".agents", ".claude"):
|
||||
assert f"{harness}/skills/wiki-demo: language block is for `en`, conventions say `de`" in out
|
||||
|
||||
instructions_cmd.sync(force=False)
|
||||
instructions_cmd.verify()
|
||||
assert "match their source" in " ".join(capsys.readouterr().out.split())
|
||||
|
||||
|
||||
def test_every_real_skill_takes_the_block():
|
||||
"""A source whose frontmatter the insertion cannot find would be published
|
||||
without the language and nothing would say so."""
|
||||
for source in instructions_cmd.skill_dirs():
|
||||
text = (source / "SKILL.md").read_text(encoding="utf-8")
|
||||
rendered = instructions_cmd.render_skill_md(text, "de")
|
||||
assert rendered != text, source.name
|
||||
assert len(_block_lines(rendered)) == 1, source.name
|
||||
|
||||
|
||||
# --- verify -----------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user